diff --git a/.github/workflows/compilation-benchmark.yaml b/.github/workflows/compilation-benchmark.yaml index ab6c5a83942..39237841bea 100644 --- a/.github/workflows/compilation-benchmark.yaml +++ b/.github/workflows/compilation-benchmark.yaml @@ -7,6 +7,9 @@ on: branches: - master - sd/beta-20 +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true jobs: benchmark: name: ${{ matrix.package }} @@ -26,6 +29,7 @@ jobs: - uses: julia-actions/setup-julia@v1 with: version: '1' + include-all-prereleases: true arch: x64 - uses: julia-actions/cache@v1 - name: Benchmark diff --git a/.github/workflows/wglmakie.yaml b/.github/workflows/wglmakie.yaml index 5e6b308fab3..6265a58419c 100644 --- a/.github/workflows/wglmakie.yaml +++ b/.github/workflows/wglmakie.yaml @@ -12,6 +12,7 @@ on: - '*' branches: - master + concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml index b07da10fe66..89e4baa9016 100644 --- a/CairoMakie/Project.toml +++ b/CairoMakie/Project.toml @@ -1,7 +1,7 @@ name = "CairoMakie" uuid = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" author = ["Simon Danisch "] -version = "0.10.12" +version = "0.11.0" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" @@ -23,7 +23,7 @@ FFTW = "1" FileIO = "1.1" FreeType = "3, 4.0" GeometryBasics = "0.4.1" -Makie = "=0.19.12" +Makie = "=0.20.0" PrecompileTools = "1.0" SHA = "0.7, 1.6, 1.7" julia = "1.3" diff --git a/CairoMakie/src/CairoMakie.jl b/CairoMakie/src/CairoMakie.jl index b5995fe1f33..f90fb9a5ab9 100644 --- a/CairoMakie/src/CairoMakie.jl +++ b/CairoMakie/src/CairoMakie.jl @@ -8,7 +8,7 @@ import Cairo using Makie: Scene, Lines, Text, Image, Heatmap, Scatter, @key_str, broadcast_foreach using Makie: convert_attribute, @extractvalue, LineSegments, to_ndim, NativeFont -using Makie: @info, @get_attribute, Combined, MakieScreen +using Makie: @info, @get_attribute, Plot, MakieScreen using Makie: to_value, to_colormap, extrema_nan using Makie.Observables using Makie: spaces, is_data_space, is_pixel_space, is_relative_space, is_clip_space diff --git a/CairoMakie/src/display.jl b/CairoMakie/src/display.jl index d5cf249942b..c93eb760dbc 100644 --- a/CairoMakie/src/display.jl +++ b/CairoMakie/src/display.jl @@ -117,6 +117,7 @@ end const DISABLED_MIMES = Set{String}() const SUPPORTED_MIMES = Set([ + map(x->string(x()), Makie.WEB_MIMES)..., "image/svg+xml", "application/pdf", "application/postscript", diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 6a036a84727..46e0e3bcc2d 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -57,7 +57,7 @@ function cairo_draw(screen::Screen, scene::Scene) end """ - is_cairomakie_atomic_plot(plot::Combined)::Bool + is_cairomakie_atomic_plot(plot::Plot)::Bool Returns whether the plot is considered atomic for the CairoMakie backend. This is overridden for `Poly`, `Band`, and `Tricontourf` so we can apply @@ -66,14 +66,14 @@ CairoMakie can treat them as atomic plots and render them directly. Plots with children are by default recursed into. This can be overridden by defining specific dispatches for `is_cairomakie_atomic_plot` for a given plot type. """ -is_cairomakie_atomic_plot(plot::Combined) = isempty(plot.plots) || to_value(get(plot, :rasterize, false)) != false +is_cairomakie_atomic_plot(plot::Plot) = isempty(plot.plots) || to_value(get(plot, :rasterize, false)) != false """ - check_parent_plots(f, plot::Combined)::Bool + check_parent_plots(f, plot::Plot)::Bool Returns whether the plot's parent tree satisfies the predicate `f`. `f` must return a `Bool` and take a plot as its only argument. """ -function check_parent_plots(f, plot::Combined) +function check_parent_plots(f, plot::Plot) if f(plot) check_parent_plots(f, parent(plot)) else @@ -87,11 +87,9 @@ end function prepare_for_scene(screen::Screen, scene::Scene) - # get the root area to correct for its pixel size when translating - root_area = Makie.root(scene).px_area[] - - root_area_height = widths(root_area)[2] - scene_area = pixelarea(scene)[] + # get the root area to correct for its size when translating + root_area_height = widths(Makie.root(scene))[2] + scene_area = viewport(scene)[] scene_height = widths(scene_area)[2] scene_x_origin, scene_y_origin = scene_area.origin @@ -103,7 +101,7 @@ function prepare_for_scene(screen::Screen, scene::Scene) top_offset = root_area_height - scene_height - scene_y_origin Cairo.translate(screen.context, scene_x_origin, top_offset) - # clip the scene to its pixelarea + # clip the scene to its viewport Cairo.rectangle(screen.context, 0, 0, widths(scene_area)...) Cairo.clip(screen.context) @@ -116,7 +114,7 @@ function draw_background(screen::Screen, scene::Scene) if scene.clear[] bg = scene.backgroundcolor[] Cairo.set_source_rgba(cr, red(bg), green(bg), blue(bg), alpha(bg)); - r = pixelarea(scene)[] + r = viewport(scene)[] Cairo.rectangle(cr, origin(r)..., widths(r)...) # background fill(cr) end @@ -124,7 +122,7 @@ function draw_background(screen::Screen, scene::Scene) foreach(child_scene-> draw_background(screen, child_scene), scene.children) end -function draw_plot(scene::Scene, screen::Screen, primitive::Combined) +function draw_plot(scene::Scene, screen::Screen, primitive::Plot) if to_value(get(primitive, :visible, true)) if isempty(primitive.plots) Cairo.save(screen.context) @@ -145,11 +143,11 @@ end # instead of the whole Scene # - Recognize when a screen is an image surface, and set scale to render the plot # at the scale of the device pixel -function draw_plot_as_image(scene::Scene, screen::Screen, primitive::Combined, scale::Number = 1) +function draw_plot_as_image(scene::Scene, screen::Screen, primitive::Plot, scale::Number = 1) # you can provide `p.rasterize = scale::Int` or `p.rasterize = true`, both of which are numbers - # Extract scene width in pixels - w, h = Int.(scene.px_area[].widths) + # Extract scene width in device indepentent units + w, h = size(scene) # Create a new Screen which renders directly to an image surface, # specifically for the plot's parent scene. scr = Screen(scene; px_per_unit = scale) @@ -178,3 +176,7 @@ end function draw_atomic(::Scene, ::Screen, x) @warn "$(typeof(x)) is not supported by cairo right now" end + +function draw_atomic(::Scene, ::Screen, x::Makie.PlotList) + # Doesn't need drawing +end diff --git a/CairoMakie/src/overrides.jl b/CairoMakie/src/overrides.jl index 6315e39e9ad..8bccdca5d3c 100644 --- a/CairoMakie/src/overrides.jl +++ b/CairoMakie/src/overrides.jl @@ -9,7 +9,7 @@ complex and slower to draw than standard paths with single color. function draw_plot(scene::Scene, screen::Screen, poly::Poly) # dispatch on input arguments to poly to use smarter drawing methods than # meshes if possible - draw_poly(scene, screen, poly, to_value.(poly.input_args)...) + return draw_poly(scene, screen, poly, to_value.(poly.args)...) end # Override `is_cairomakie_atomic_plot` to allow `poly` to remain a unit, diff --git a/CairoMakie/src/precompiles.jl b/CairoMakie/src/precompiles.jl index b2c6ab3155d..a654e938544 100644 --- a/CairoMakie/src/precompiles.jl +++ b/CairoMakie/src/precompiles.jl @@ -15,3 +15,9 @@ let include(shared_precompile) end end +precompile(draw_atomic_scatter, (Scene, Cairo.CairoContext, Tuple{typeof(identity),typeof(identity)}, + Vector{ColorTypes.RGBA{Float32}}, Vec{2,Float32}, ColorTypes.RGBA{Float32}, + Float32, BezierPath, Vec{2,Float32}, Quaternionf, + Mat4f, Vector{Point{2,Float32}}, + Mat4f, Makie.FreeTypeAbstraction.FTFont, Symbol, + Symbol)) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index f636b129b8c..3d920610c44 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -49,8 +49,8 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio Cairo.set_dash(ctx, pattern) end - if primitive isa Lines && primitive.input_args[1][] isa BezierPath - return draw_bezierpath_lines(ctx, primitive.input_args[1][], scene, color, space, model, linewidth) + if primitive isa Lines && to_value(primitive.args[1]) isa BezierPath + return draw_bezierpath_lines(ctx, to_value(primitive.args[1]), scene, color, space, model, linewidth) end if color isa AbstractArray || linewidth isa AbstractArray @@ -496,13 +496,14 @@ end function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Text{<:Tuple{<:Union{AbstractArray{<:Makie.GlyphCollection}, Makie.GlyphCollection}}})) ctx = screen.context @get_attribute(primitive, (rotation, model, space, markerspace, offset)) + transform_marker = to_value(get(primitive, :transform_marker, true))::Bool position = primitive.position[] # use cached glyph info glyph_collection = to_value(primitive[1]) draw_glyph_collection( scene, ctx, position, glyph_collection, remove_billboard(rotation), - model, space, markerspace, offset, primitive.transformation + model, space, markerspace, offset, primitive.transformation, transform_marker ) nothing @@ -511,21 +512,23 @@ end function draw_glyph_collection( scene, ctx, positions, glyph_collections::AbstractArray, rotation, - model::Mat, space, markerspace, offset, transformation + model::Mat, space, markerspace, offset, transformation, transform_marker ) # TODO: why is the Ref around model necessary? doesn't broadcast_foreach handle staticarrays matrices? broadcast_foreach(positions, glyph_collections, rotation, Ref(model), space, markerspace, offset) do pos, glayout, ro, mo, sp, msp, off - draw_glyph_collection(scene, ctx, pos, glayout, ro, mo, sp, msp, off, transformation) + draw_glyph_collection(scene, ctx, pos, glayout, ro, mo, sp, msp, off, transformation, transform_marker) end end _deref(x) = x _deref(x::Ref) = x[] -function draw_glyph_collection(scene, ctx, position, glyph_collection, rotation, _model, space, markerspace, offsets, transformation) +function draw_glyph_collection( + scene, ctx, position, glyph_collection, rotation, _model, space, + markerspace, offsets, transformation, transform_marker) glyphs = glyph_collection.glyphs glyphoffsets = glyph_collection.origins @@ -537,7 +540,7 @@ function draw_glyph_collection(scene, ctx, position, glyph_collection, rotation, strokecolors = glyph_collection.strokecolors model = _deref(_model) - model33 = model[Vec(1, 2, 3), Vec(1, 2, 3)] + model33 = transform_marker ? model[Vec(1, 2, 3), Vec(1, 2, 3)] : Mat3f(I) id = Mat4f(I) glyph_pos = let @@ -822,8 +825,16 @@ function draw_mesh2D(scene, screen, per_face_cols, space::Symbol, transform_func # This is a hack, which needs cleaning up in the Mesh plot type! for (f, (c1, c2, c3)) in zip(fs, per_face_cols) - pattern = Cairo.CairoPatternMesh() + t1, t2, t3 = project_position.(scene, (transform_func,), space, vs[f], (model,)) #triangle points + + # don't draw any mesh faces with NaN components. + if isnan(t1) || isnan(t2) || isnan(t3) + continue + end + + pattern = Cairo.CairoPatternMesh() + Cairo.mesh_pattern_begin_patch(pattern) Cairo.mesh_pattern_move_to(pattern, t1...) @@ -851,13 +862,13 @@ end nan2zero(x) = !isnan(x) * x -function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f0) +function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f0, rotation = Mat4f(I)) @get_attribute(attributes, (shading, diffuse, specular, shininess, faceculling)) matcap = to_value(get(attributes, :matcap, nothing)) meshpoints = decompose(Point3f, mesh)::Vector{Point3f} meshfaces = decompose(GLTriangleFace, mesh)::Vector{GLTriangleFace} - meshnormals = decompose_normals(mesh)::Vector{Vec3f} + meshnormals = decompose_normals(mesh)::Vector{Vec3f} # note: can be made NaN-aware. meshuvs = texturecoordinates(mesh)::Union{Nothing, Vector{Vec2f}} # Priorize colors of the mesh if present @@ -868,43 +879,61 @@ function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f model = attributes.model[]::Mat4f space = to_value(get(attributes, :space, :data))::Symbol func = Makie.transform_func(attributes) + + # TODO: assume Symbol here after this has been deprecated for a while + if shading isa Bool + @warn "`shading::Bool` is deprecated. Use `shading = NoShading` instead of false and `shading = FastShading` or `shading = MultiLightShading` instead of true." + shading_bool = shading + else + shading_bool = shading != NoShading + end + draw_mesh3D( - scene, screen, space, func, meshpoints, meshfaces, meshnormals, per_face_col, pos, scale, - model, shading::Bool, diffuse::Vec3f, + scene, screen, space, func, meshpoints, meshfaces, meshnormals, per_face_col, + pos, scale, rotation, + model, shading_bool::Bool, diffuse::Vec3f, specular::Vec3f, shininess::Float32, faceculling::Int ) end function draw_mesh3D( - scene, screen, space, transform_func, meshpoints, meshfaces, meshnormals, per_face_col, pos, scale, + scene, screen, space, transform_func, meshpoints, meshfaces, meshnormals, per_face_col, + pos, scale, rotation, model, shading, diffuse, specular, shininess, faceculling ) ctx = screen.context - view = ifelse(is_data_space(space), scene.camera.view[], Mat4f(I)) - projection = Makie.space_to_clip(scene.camera, space, false) + projectionview = Makie.space_to_clip(scene.camera, space, true) + eyeposition = scene.camera.eyeposition[] i = Vec(1, 2, 3) - normalmatrix = transpose(inv(view[i, i] * model[i, i])) - - # Mesh data - # transform to view/camera space + normalmatrix = transpose(inv(model[i, i])) + local_model = rotation * Makie.scalematrix(Vec3f(scale)) # pass transform_func as argument to function, so that we get a function barrier # and have `transform_func` be fully typed inside closure vs = broadcast(meshpoints, (transform_func,)) do v, f # Should v get a nan2zero? v = Makie.apply_transform(f, v, space) - p4d = to_ndim(Vec4f, scale .* to_ndim(Vec3f, v, 0f0), 1f0) - view * (model * p4d .+ to_ndim(Vec4f, pos, 0f0)) + p4d = to_ndim(Vec4f, to_ndim(Vec3f, v, 0f0), 1f0) + model * (local_model * p4d .+ to_ndim(Vec4f, pos, 0f0)) end ns = map(n -> normalize(normalmatrix * n), meshnormals) - # Liight math happens in view/camera space - pointlight = Makie.get_point_light(scene) - lightposition = if !isnothing(pointlight) - pointlight.position[] + + # Light math happens in view/camera space + dirlight = Makie.get_directional_light(scene) + if !isnothing(dirlight) + lightdirection = if dirlight.camera_relative + T = inv(scene.camera.view[][Vec(1,2,3), Vec(1,2,3)]) + normalize(T * dirlight.direction[]) + else + normalize(dirlight.direction[]) + end + c = dirlight.color[] + light_color = Vec3f(red(c), green(c), blue(c)) else - Vec3f(0) + lightdirection = Vec3f(0,0,-1) + light_color = Vec3f(0) end ambientlight = Makie.get_ambient_light(scene) @@ -915,11 +944,9 @@ function draw_mesh3D( Vec3f(0) end - lightpos = (view * to_ndim(Vec4f, lightposition, 1.0))[Vec(1, 2, 3)] - # Camera to screen space ts = map(vs) do v - clip = projection * v + clip = projectionview * v @inbounds begin p = (clip ./ clip[4])[Vec(1, 2)] p_yflip = Vec2f(p[1], -p[2]) @@ -929,6 +956,9 @@ function draw_mesh3D( return Vec3f(p[1], p[2], clip[3]) end + # vs are used as camdir (camera to vertex) for light calculation (in world space) + vs = map(v -> normalize(v[i] - eyeposition), vs) + # Approximate zorder average_zs = map(f -> average_z(ts, f), meshfaces) zorder = sortperm(average_zs) @@ -936,25 +966,24 @@ function draw_mesh3D( # Face culling zorder = filter(i -> any(last.(ns[meshfaces[i]]) .> faceculling), zorder) - draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightpos, shininess, diffuse, ambient, specular) + draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightdirection, light_color, shininess, diffuse, ambient, specular) return end -function _calculate_shaded_vertexcolors(N, v, c, lightpos, ambient, diffuse, specular, shininess) - L = normalize(lightpos .- v[Vec(1,2,3)]) - diff_coeff = max(dot(L, N), 0f0) - H = normalize(L + normalize(-v[Vec(1, 2, 3)])) - spec_coeff = max(dot(H, N), 0f0)^shininess +function _calculate_shaded_vertexcolors(N, v, c, lightdir, light_color, ambient, diffuse, specular, shininess) + L = lightdir + diff_coeff = max(dot(L, -N), 0f0) + H = normalize(L + v) + spec_coeff = max(dot(H, -N), 0f0)^shininess c = RGBAf(c) # if this is one expression it introduces allocations?? - new_c_part1 = (ambient .+ diff_coeff .* diffuse) .* Vec3f(c.r, c.g, c.b) #.+ - new_c = new_c_part1 .+ specular * spec_coeff + new_c_part1 = (ambient .+ light_color .* diff_coeff .* diffuse) .* Vec3f(c.r, c.g, c.b) #.+ + new_c = new_c_part1 .+ light_color .* specular * spec_coeff RGBAf(new_c..., c.alpha) end -function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightpos, shininess, diffuse, ambient, specular) +function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightdir, light_color, shininess, diffuse, ambient, specular) for k in reverse(zorder) - pattern = Cairo.CairoPatternMesh() f = meshfaces[k] # avoid SizedVector through Face indexing @@ -962,6 +991,11 @@ function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, t2 = ts[f[2]] t3 = ts[f[3]] + # skip any mesh segments with NaN points. + if isnan(t1) || isnan(t2) || isnan(t3) + continue + end + facecolors = per_face_col[k] # light calculation if shading @@ -971,7 +1005,7 @@ function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, N = ns[f[i]] v = vs[f[i]] c = facecolors[i] - _calculate_shaded_vertexcolors(N, v, c, lightpos, ambient, diffuse, specular, shininess) + _calculate_shaded_vertexcolors(N, v, c, lightdir, light_color, ambient, diffuse, specular, shininess) end else c1, c2, c3 = facecolors @@ -983,6 +1017,8 @@ function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, # c2 = RGB(n2...) # c3 = RGB(n3...) + pattern = Cairo.CairoPatternMesh() + Cairo.mesh_pattern_begin_patch(pattern) Cairo.mesh_pattern_move_to(pattern, t1[1], t1[2]) @@ -1009,7 +1045,7 @@ end function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Makie.Surface)) # Pretend the surface plot is a mesh plot and plot that instead - mesh = surface2mesh(primitive[1][], primitive[2][], primitive[3][]) + mesh = Makie.surface2mesh(primitive[1][], primitive[2][], primitive[3][]) old = primitive[:color] if old[] === nothing primitive[:color] = primitive[3] @@ -1022,14 +1058,6 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki return nothing end -function surface2mesh(xs, ys, zs::AbstractMatrix) - ps = Makie.matrix_grid(p-> nan2zero.(p), xs, ys, zs) - rect = Tesselation(Rect2f(0, 0, 1, 1), size(zs)) - faces = decompose(QuadFace{Int}, rect) - uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), decompose_uv(rect)) - uvm = GeometryBasics.Mesh(GeometryBasics.meta(ps; uv=uv), faces) - return GeometryBasics.normal_mesh(uvm) -end ################################################################################ # MeshScatter # @@ -1060,25 +1088,24 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki ) - if !(rotations isa Vector) - R = Makie.rotationmatrix4(to_rotation(rotations)) - submesh[:model] = model * R - end + submesh[:model] = model scales = primitive[:markersize][] for i in zorder p = pos[i] if color isa AbstractVector submesh[:calculated_colors] = color[i] end - if rotations isa Vector - R = Makie.rotationmatrix4(to_rotation(rotations[i])) - submesh[:model] = model * R - end scale = markersize isa Vector ? markersize[i] : markersize + rotation = if rotations isa Vector + Makie.rotationmatrix4(to_rotation(rotations[i])) + else + Makie.rotationmatrix4(to_rotation(rotations)) + end draw_mesh3D( scene, screen, submesh, marker, pos = p, - scale = scale isa Real ? Vec3f(scale) : to_ndim(Vec3f, scale, 1f0) + scale = scale isa Real ? Vec3f(scale) : to_ndim(Vec3f, scale, 1f0), + rotation = rotation ) end diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index 16de9cca42f..c11f5861af9 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -1,6 +1,6 @@ using Base.Docs: doc -@enum RenderType SVG IMAGE PDF EPS +@enum RenderType SVG IMAGE PDF EPS HTML Base.convert(::Type{RenderType}, ::MIME{SYM}) where SYM = mime_to_rendertype(SYM) function Base.convert(::Type{RenderType}, type::String) @@ -12,6 +12,8 @@ function Base.convert(::Type{RenderType}, type::String) return PDF elseif type == "eps" return EPS + elseif type in ("html", "text/html", "application/vnd.webio.application+html", "application/prs.juno.plotpane+html", "juliavscode/html") + return HTML else error("Unsupported cairo render type: $type") end @@ -22,6 +24,7 @@ function to_mime(type::RenderType) type == SVG && return MIME("image/svg+xml") type == PDF && return MIME("application/pdf") type == EPS && return MIME("application/postscript") + type == HTML && return MIME("text/html") return MIME("image/png") end @@ -35,6 +38,8 @@ function mime_to_rendertype(mime::Symbol)::RenderType return PDF elseif mime == Symbol("application/postscript") return EPS + elseif mime in (Symbol("text/html"), Symbol("text/html"), Symbol("application/vnd.webio.application+html"), Symbol("application/prs.juno.plotpane+html"), Symbol("juliavscode/html")) + return HTML else error("Unsupported mime: $mime") end @@ -55,7 +60,7 @@ function surface_from_output_type(type::RenderType, io, w, h) return Cairo.CairoPDFSurface(io, w, h) elseif type === EPS return Cairo.CairoEPSSurface(io, w, h) - elseif type === IMAGE + elseif type === IMAGE || type === HTML img = fill(ARGB32(0, 0, 0, 0), w, h) return Cairo.CairoImageSurface(img) else @@ -76,8 +81,8 @@ end to_cairo_antialias(aa::Int) = aa """ -* `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/). +* `px_per_unit = 2.0`: see [figure docs](https://docs.makie.org/stable/documentation/figure_size/). +* `pt_per_unit = 0.75`: see [figure 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. """ @@ -120,9 +125,7 @@ function activate!(; inline=LAST_INLINE[], type="png", screen_config...) # So, if we want to prefer the png mime, we disable the mimes that are usually higher up in the stack. disable_mime!("svg", "pdf") elseif type == "svg" - # SVG is usually pretty high up the priority, so we can just enable all mimes - # If we implement html display for CairoMakie, we might need to disable that. - disable_mime!() + disable_mime!("text/html", "application/vnd.webio.application+html", "application/prs.juno.plotpane+html", "juliavscode/html") else enable_only_mime!(type) end @@ -245,7 +248,7 @@ function Makie.apply_screen_config!(screen::Screen, config::ScreenConfig, scene: end function Screen(scene::Scene; screen_config...) - config = Makie.merge_screen_config(ScreenConfig, screen_config) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) return Screen(scene, config) end @@ -303,6 +306,7 @@ function Makie.colorbuffer(screen::Screen) end function Makie.colorbuffer(screen::Screen{IMAGE}) + Makie.push_screen!(screen.scene, screen) empty!(screen) cairo_draw(screen, screen.scene) return PermutedDimsArray(screen.surface.data, (2, 1)) diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index c602643477f..e22b8938dc0 100644 --- a/CairoMakie/src/utils.jl +++ b/CairoMakie/src/utils.jl @@ -118,6 +118,15 @@ end to_uint32_color(c) = reinterpret(UInt32, convert(ARGB32, premultiplied_rgba(c))) +# handle patterns +function Cairo.CairoPattern(color::Makie.AbstractPattern) + # the Cairo y-coordinate are fliped + bitmappattern = reverse!(ARGB32.(Makie.to_image(color)); dims=2) + cairoimage = Cairo.CairoImageSurface(bitmappattern) + cairopattern = Cairo.CairoPattern(cairoimage) + return cairopattern +end + ######################################## # Common color utilities # ######################################## @@ -217,7 +226,7 @@ function per_face_colors(_color, matcap, faces, normals, uv) wsize = reverse(size(color)) wh = wsize .- 1 cvec = map(uv) do uv - x, y = clamp.(round.(Int, Tuple(uv) .* wh) .+ 1, 1, wh) + x, y = clamp.(round.(Int, Tuple(uv) .* wh) .+ 1, 1, wsize) return color[end - (y - 1), x] end # TODO This is wrong and doesn't actually interpolate @@ -231,14 +240,10 @@ function mesh_pattern_set_corner_color(pattern, id, c::Colorant) Cairo.mesh_pattern_set_corner_color_rgba(pattern, id, rgbatuple(c)...) end -# not piracy -function Cairo.CairoPattern(color::Makie.AbstractPattern) - # the Cairo y-coordinate are fliped - bitmappattern = reverse!(ARGB32.(Makie.to_image(color)); dims=2) - cairoimage = Cairo.CairoImageSurface(bitmappattern) - cairopattern = Cairo.CairoPattern(cairoimage) - return cairopattern -end +################################################################################ +# Font handling # +################################################################################ + """ Finds a font that can represent the unicode character! diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index 84330b132e7..d3dcc4c6c93 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -37,7 +37,7 @@ include(joinpath(@__DIR__, "rasterization_tests.jl")) @testset "saving pdf two times" begin # https://github.com/MakieOrg/Makie.jl/issues/2433 - fig = Figure(resolution=(480, 792)) + fig = Figure(size = (480, 792)) ax = Axis(fig[1, 1]) # The IO was shared between screens, which left the second figure empty save("fig.pdf", fig, pt_per_unit=0.5) @@ -52,17 +52,17 @@ include(joinpath(@__DIR__, "rasterization_tests.jl")) # https://github.com/MakieOrg/Makie.jl/issues/2438 # This bug was caused by using the screen size of the pdf screen, which # has a different device_scaling_factor, and therefore a different screen size - fig = scatter(1:4, figure=(; resolution=(800, 800))) + fig = scatter(1:4, figure=(; size = (800, 800))) save("test.pdf", fig) size(Makie.colorbuffer(fig)) == (800, 800) rm("test.pdf") end @testset "switching from pdf screen to png, save" begin - fig = scatter(1:4, figure=(; resolution=(800, 800))) + fig = scatter(1:4, figure=(; size = (800, 800))) save("test.pdf", fig) save("test.png", fig) - @test size(load("test.png")) == (800, 800) + @test size(load("test.png")) == (1600, 1600) rm("test.pdf") rm("test.png") end @@ -89,7 +89,7 @@ include(joinpath(@__DIR__, "rasterization_tests.jl")) @testset "changing resolution of same format" begin # see: https://github.com/MakieOrg/Makie.jl/issues/2433 # and: https://github.com/MakieOrg/AlgebraOfGraphics.jl/pull/441 - scene = Scene(resolution=(800, 800)); + scene = Scene(size = (800, 800)); load_save(s; kw...) = (save("test.png", s; kw...); load("test.png")) @test size(load_save(scene, px_per_unit=2)) == (1600, 1600) @test size(load_save(scene, px_per_unit=1)) == (800, 800) @@ -122,7 +122,7 @@ end @testset "VideoStream & screen options" begin N = 3 points = Observable(Point2f[]) - f, ax, pl = scatter(points, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(resolution=(600, 800),)) + f, ax, pl = scatter(points, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(size=(600, 800),)) vio = Makie.VideoStream(f; format="mp4", px_per_unit=2.0, backend=CairoMakie) @test vio.screen isa CairoMakie.Screen{CairoMakie.IMAGE} @test size(vio.screen) == size(f.scene) .* 2 @@ -134,7 +134,6 @@ end rm("test.mp4") end -using ReferenceTests excludes = Set([ "Colored Mesh", @@ -191,7 +190,7 @@ excludes = Set([ functions = [:volume, :volume!, :uv_mesh] @testset "refimages" begin - CairoMakie.activate!(type = "png") + CairoMakie.activate!(type = "png", px_per_unit = 1) ReferenceTests.mark_broken_tests(excludes, functions=functions) recorded_files, recording_dir = @include_reference_tests "refimages.jl" missing_images, scores = ReferenceTests.record_comparison(recording_dir) diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index 91d704be82f..e3ac68e4827 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -1,6 +1,6 @@ name = "GLMakie" uuid = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" -version = "0.8.12" +version = "0.9.0" [deps] ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" @@ -27,9 +27,9 @@ Colors = "0.11, 0.12" FileIO = "1.6" FixedPointNumbers = "0.7, 0.8" FreeTypeAbstraction = "0.10" -GLFW = "3" +GLFW = "3.3" GeometryBasics = "0.4.1" -Makie = "=0.19.12" +Makie = "=0.20.0" MeshIO = "0.4" ModernGL = "1" Observables = "0.5.1" diff --git a/GLMakie/assets/shader/distance_shape.frag b/GLMakie/assets/shader/distance_shape.frag index 1221978a1be..6fdeda71b3f 100644 --- a/GLMakie/assets/shader/distance_shape.frag +++ b/GLMakie/assets/shader/distance_shape.frag @@ -25,13 +25,12 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d uniform float stroke_width; uniform float glow_width; uniform int shape; // shape is a uniform for now. Making them a in && using them for control flow is expected to kill performance -uniform vec2 resolution; +uniform float px_per_unit; uniform bool transparent_picking; flat in float f_viewport_from_u_scale; flat in float f_distancefield_scale; flat in vec4 f_color; -flat in vec4 f_bg_color; flat in vec4 f_stroke_color; flat in vec4 f_glow_color; flat in uvec2 f_id; @@ -90,7 +89,7 @@ float ellipse(vec2 uv, vec2 scale) // 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){ - color = mix(color, fillcolor, infill); -} -void fill(vec4 c, sampler2D image, vec2 uv, float infill, inout vec4 color){ - color.rgba = mix(color, texture(image, uv.yx), infill); -} -void fill(vec4 c, sampler2DArray image, vec2 uv, float infill, inout vec4 color){ - color = mix(color, texture(image, vec3(uv.yx, f_primitive_index)), infill); +vec4 fill(vec4 fillcolor, Nothing image, vec2 uv) { return fillcolor; } +vec4 fill(vec4 c, sampler2D image, vec2 uv) { return texture(image, uv.yx); } +vec4 fill(vec4 c, sampler2DArray image, vec2 uv) { + return texture(image, vec3(uv.yx, f_primitive_index)); } @@ -130,8 +125,10 @@ void stroke(vec4 strokecolor, float signed_distance, float width, inout vec4 col void glow(vec4 glowcolor, float signed_distance, float inside, inout vec4 color){ if (glow_width > 0.0){ - float outside = (abs(signed_distance)-stroke_width)/glow_width; - float alpha = 1-outside; + float s_stroke_width = px_per_unit * stroke_width; + float s_glow_width = px_per_unit * glow_width; + float outside = (abs(signed_distance) - s_stroke_width) / s_glow_width; + float alpha = 1 - outside; color = mix(vec4(glowcolor.rgb, glowcolor.a*alpha), color, inside); } } @@ -180,13 +177,21 @@ void main(){ // See notes in geometry shader where f_viewport_from_u_scale is computed. signed_distance *= f_viewport_from_u_scale; - float inside_start = max(-stroke_width, 0.0); + float s_stroke_width = px_per_unit * stroke_width; + float inside_start = max(-s_stroke_width, 0.0); float inside = aastep(inside_start, signed_distance); - vec4 final_color = f_bg_color; - fill(f_color, image, tex_uv, inside, final_color); - stroke(f_stroke_color, signed_distance, -stroke_width, final_color); - glow(f_glow_color, signed_distance, aastep(-stroke_width, signed_distance), final_color); + // For the initial coloring we can use the base pixel color and modulate + // its alpha value to create the shape set by the signed distance field. (i.e. inside) + vec4 final_color = fill(f_color, image, tex_uv); + final_color.a = final_color.a * inside; + + // Stroke and glow need to also modulate colors (rgb) to smoothly transition + // from one to another. + stroke(f_stroke_color, signed_distance, -s_stroke_width, final_color); + glow(f_glow_color, signed_distance, aastep(-s_stroke_width, signed_distance), final_color); + + // TODO: In 3D, we should arguably discard fragments outside the sprite // But note that this may interfere with object picking. // if (final_color == f_bg_color) diff --git a/GLMakie/assets/shader/fragment_output.frag b/GLMakie/assets/shader/fragment_output.frag index 7fb14e78a9a..837d5ccc3ec 100644 --- a/GLMakie/assets/shader/fragment_output.frag +++ b/GLMakie/assets/shader/fragment_output.frag @@ -13,7 +13,7 @@ layout(location=1) out uvec2 fragment_groupid; in vec3 o_view_pos; -in vec3 o_normal; +in vec3 o_view_normal; void write2framebuffer(vec4 color, uvec2 id){ if(color.a <= 0.0) @@ -34,7 +34,7 @@ void write2framebuffer(vec4 color, uvec2 id){ // // if transparency == false && ssao = true // fragment_color = color; // fragment_position = o_view_pos; - // fragment_normal_occlusion.xyz = o_normal; + // fragment_normal_occlusion.xyz = o_view_normal; // // else // fragment_color = color; diff --git a/GLMakie/assets/shader/heatmap.vert b/GLMakie/assets/shader/heatmap.vert index df15cab1327..380bf802328 100644 --- a/GLMakie/assets/shader/heatmap.vert +++ b/GLMakie/assets/shader/heatmap.vert @@ -13,7 +13,7 @@ out vec2 o_uv; flat out uvec2 o_objectid; out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; ivec2 ind2sub(ivec2 dim, int linearindex){ return ivec2(linearindex % dim.x, linearindex / dim.x); @@ -22,7 +22,7 @@ ivec2 ind2sub(ivec2 dim, int linearindex){ void main(){ //Outputs for ssao, which we don't use for 2d shaders like heatmap/image o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); int index = gl_InstanceID; vec2 offset = vertices; diff --git a/GLMakie/assets/shader/lighting.frag b/GLMakie/assets/shader/lighting.frag new file mode 100644 index 00000000000..015ad495f97 --- /dev/null +++ b/GLMakie/assets/shader/lighting.frag @@ -0,0 +1,212 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +// Sets which shading procedures to use +// Options: +// NO_SHADING - skip shading calculation, handled outside +// FAST_SHADING - single point light (forward rendering) +// MULTI_LIGHT_SHADING - simple shading with multiple lights (forward rendering) +{{shading}} + + +// Shared uniforms, inputs and functions +#if defined FAST_SHADING || defined MULTI_LIGHT_SHADING + +// Generic uniforms +uniform vec3 diffuse; +uniform vec3 specular; +uniform float shininess; + +uniform float backlight; + +in vec3 o_camdir; +in vec3 o_world_pos; + +float smooth_zero_max(float x) { + // This is a smoothed version of max(value, 0.0) where -1 <= value <= 1 + // This comes from: + // c = 2 ^ -a # normalizes power w/o swaps + // xswap = (1 / c / a)^(1 / (a - 1)) - 1 # xval with derivative 1 + // yswap = c * (xswap+1) ^ a # yval with derivative 1 + // ifelse.(xs .< yswap, c .* (xs .+ 1 .+ xswap .- yswap) .^ a, xs) + // a = 16 constants: (harder edge) + // const float c = 0.0000152587890625, xswap = 0.7411011265922482, yswap = 0.10881882041201549; + // a = 8 constants: (softer edge) + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + return x < yswap ? c * pow(x + shift, 8) : x; +} + +vec3 blinn_phong(vec3 light_color, vec3 light_dir, vec3 camdir, vec3 normal, vec3 color) { + // diffuse coefficient (how directly does light hits the surface) + float diff_coeff = smooth_zero_max(dot(light_dir, -normal)) + + backlight * smooth_zero_max(dot(light_dir, normal)); + + // DEBUG - visualize diff_coeff, i.e. the angle between light and normals + // if (diff_coeff > 0.999) + // return vec3(0, 0, 1); + // else + // return vec3(1 - diff_coeff,diff_coeff, 0.05); + + // specular coefficient (does reflected light bounce into camera?) + vec3 H = normalize(light_dir + camdir); + float spec_coeff = pow(max(dot(H, -normal), 0.0), shininess) + + backlight * pow(max(dot(H, normal), 0.0), shininess); + if (diff_coeff <= 0.0 || isnan(spec_coeff)) + spec_coeff = 0.0; + + return light_color * vec3(diffuse * diff_coeff * color + specular * spec_coeff); +} + +#else // glsl fails to compile if the shader is just empty + +vec3 illuminate(vec3 normal, vec3 base_color); + +#endif + + +//////////////////////////////////////////////////////////////////////////////// +// FAST_SHADING // +//////////////////////////////////////////////////////////////////////////////// + + +#ifdef FAST_SHADING + +uniform vec3 ambient; +uniform vec3 light_color; +uniform vec3 light_direction; + +vec3 illuminate(vec3 world_pos, vec3 camdir, vec3 normal, vec3 base_color) { + vec3 shaded_color = blinn_phong(light_color, light_direction, camdir, normal, base_color); + return ambient * base_color + shaded_color; +} + +vec3 illuminate(vec3 normal, vec3 base_color) { + return illuminate(o_world_pos, normalize(o_camdir), normal, base_color); +} + +#endif + + +//////////////////////////////////////////////////////////////////////////////// +// MULTI_LIGHT_SHADING // +//////////////////////////////////////////////////////////////////////////////// + + +#ifdef MULTI_LIGHT_SHADING + +{{MAX_LIGHTS}} +{{MAX_LIGHT_PARAMETERS}} + +// differentiating different light sources +const int UNDEFINED = 0; +const int Ambient = 1; +const int PointLight = 2; +const int DirectionalLight = 3; +const int SpotLight = 4; +const int RectLight = 5; + +// light parameters (maybe invalid depending on light type) +uniform int N_lights; +uniform int light_types[MAX_LIGHTS]; +uniform vec3 light_colors[MAX_LIGHTS]; +uniform float light_parameters[MAX_LIGHT_PARAMETERS]; + +vec3 calc_point_light(vec3 light_color, int idx, vec3 world_pos, vec3 camdir, vec3 normal, vec3 color) { + // extract args + vec3 position = vec3(light_parameters[idx], light_parameters[idx+1], light_parameters[idx+2]); + vec2 param = vec2(light_parameters[idx+3], light_parameters[idx+4]); + + // calculate light direction and distance + vec3 light_vec = world_pos - position; + + float dist = length(light_vec); + vec3 light_dir = normalize(light_vec); + + // How weak has the light gotten due to distance + // float attentuation = 1.0 / (1.0 + dist * dist * dist); + float attentuation = 1.0 / (1.0 + param.x * dist + param.y * dist * dist); + + return attentuation * blinn_phong(light_color, light_dir, camdir, normal, color); +} + +vec3 calc_directional_light(vec3 light_color, int idx, vec3 camdir, vec3 normal, vec3 color) { + vec3 light_dir = vec3(light_parameters[idx], light_parameters[idx+1], light_parameters[idx+2]); + return blinn_phong(light_color, light_dir, camdir, normal, color); +} + +vec3 calc_spot_light(vec3 light_color, int idx, vec3 world_pos, vec3 camdir, vec3 normal, vec3 color) { + // extract args + vec3 position = vec3(light_parameters[idx], light_parameters[idx+1], light_parameters[idx+2]); + vec3 spot_light_dir = normalize(vec3(light_parameters[idx+3], light_parameters[idx+4], light_parameters[idx+5])); + float inner_angle = light_parameters[idx+6]; // cos applied + float outer_angle = light_parameters[idx+7]; // cos applied + + vec3 light_dir = normalize(world_pos - position); + float intensity = smoothstep(outer_angle, inner_angle, dot(light_dir, spot_light_dir)); + + return intensity * blinn_phong(light_color, light_dir, camdir, normal, color); +} + +vec3 calc_rect_light(vec3 light_color, int idx, vec3 world_pos, vec3 camdir, vec3 normal, vec3 color) { + // extract args + vec3 origin = vec3(light_parameters[idx], light_parameters[idx+1], light_parameters[idx+2]); + vec3 u1 = vec3(light_parameters[idx+3], light_parameters[idx+4], light_parameters[idx+5]); + vec3 u2 = vec3(light_parameters[idx+6], light_parameters[idx+7], light_parameters[idx+8]); + vec3 light_dir = vec3(light_parameters[idx+9], light_parameters[idx+10], light_parameters[idx+11]); + + // Find t such that = + // to find the point p = world_pos + t * light_dir = origin + w1 * u1 + w2 * u2 + 0 * light_dir. + // Then check if p is inside the rectangle by computing w1 and w2. + float t = dot(origin - world_pos, light_dir); + vec3 dir = world_pos + t * light_dir - origin; + float w1 = dot(dir, u1) / dot(u1, u1); + float w2 = dot(dir, u2) / dot(u2, u2); + + // mask out light rays that do not come from inside the shape + float intensity = smoothstep(0.45, 0.55, 1-abs(w1)) * smoothstep(0.45, 0.55, 1-abs(w2)); + + // If we do not mask the plane we may want to consider light rays coming from + // the closest edge. + // vec3 position = origin + clamp(w1, -0.5, 0.5) * u1 + clamp(w2, -0.5, 0.5) * u2; + // vec3 light_dir = normalize(world_pos - position); + + return intensity * blinn_phong(light_color, light_dir, camdir, normal, color); +} + +vec3 illuminate(vec3 world_pos, vec3 camdir, vec3 normal, vec3 base_color) { + vec3 final_color = vec3(0); + int idx = 0; + for (int i = 0; i < min(N_lights, MAX_LIGHTS); i++) { + switch (light_types[i]) { + case Ambient: + final_color += light_colors[i] * base_color; + break; + case PointLight: + final_color += calc_point_light(light_colors[i], idx, world_pos, camdir, normal, base_color); + idx += 5; // 3 position, 2 attenuation params + break; + case DirectionalLight: + final_color += calc_directional_light(light_colors[i], idx, camdir, normal, base_color); + idx += 3; // 3 direction + break; + case SpotLight: + final_color += calc_spot_light(light_colors[i], idx, world_pos, camdir, normal, base_color); + idx += 8; // 3 position, 3 direction, 1 parameter + break; + case RectLight: + final_color += calc_rect_light(light_colors[i], idx, world_pos, camdir, normal, base_color); + idx += 12; + break; + default: + return vec3(1,0,1); // debug magenta + } + } + return final_color; +} + +vec3 illuminate(vec3 normal, vec3 base_color) { + return illuminate(o_world_pos, normalize(o_camdir), normal, base_color); +} + +#endif diff --git a/GLMakie/assets/shader/line_segment.geom b/GLMakie/assets/shader/line_segment.geom index 2c0f8d473ef..ad8b9399b13 100644 --- a/GLMakie/assets/shader/line_segment.geom +++ b/GLMakie/assets/shader/line_segment.geom @@ -47,12 +47,12 @@ void emit_vertex(vec3 position, vec2 uv, int index) } out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; void main(void) { o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); // get the four vertices passed to the shader: vec3 p0 = screen_space(gl_in[0].gl_Position); // start of previous segment @@ -72,8 +72,8 @@ void main(void) vec3 AA_offset = AA_THICKNESS * v0; float AA = AA_THICKNESS * px2u; - /* 0 v0 l - | --> | + /* 0 v0 l + | --> | -thickness_aa0 - .----------------------------------. - -thickness_aa1 -g_thickness[0] - | .------------------------------. | - -g_thickness[1] | | | | @@ -95,8 +95,8 @@ void main(void) emit_vertex(p1 + thickness_aa1 * n0 + AA_offset, vec2(2*u + AA, -thickness_aa1), 1); emit_vertex(p1 - thickness_aa1 * n0 + AA_offset, vec2(2*u + AA, thickness_aa1), 1); #else - // For patterned lines AA is mostly done by the pattern sampling. We - // still set f_uv_minmax here to ensure that cut off patterns als have + // For patterned lines AA is mostly done by the pattern sampling. We + // still set f_uv_minmax here to ensure that cut off patterns als have // anti-aliasing at the start/end of this segment f_uv_minmax = vec2(0, u); emit_vertex(p0 + thickness_aa0 * n0 - AA_offset, vec2( - AA, -thickness_aa0), 0); diff --git a/GLMakie/assets/shader/line_segment.vert b/GLMakie/assets/shader/line_segment.vert index d93ee008ca3..b158327fd20 100644 --- a/GLMakie/assets/shader/line_segment.vert +++ b/GLMakie/assets/shader/line_segment.vert @@ -16,6 +16,7 @@ in float lastlen; uniform mat4 projectionview, model; uniform uint objectid; uniform float depth_shift; +uniform float px_per_unit; out uvec2 g_id; out vec4 g_color; @@ -42,7 +43,7 @@ void main() int index = gl_VertexID; g_id = uvec2(objectid, index+1); g_color = to_color(color, color_map, color_norm, index); - g_thickness = thickness; + g_thickness = px_per_unit * thickness; gl_Position = projectionview * model * to_vec4(vertex); gl_Position.z += gl_Position.w * depth_shift; } diff --git a/GLMakie/assets/shader/lines.geom b/GLMakie/assets/shader/lines.geom index 2670ad655ae..f5683063430 100644 --- a/GLMakie/assets/shader/lines.geom +++ b/GLMakie/assets/shader/lines.geom @@ -24,7 +24,7 @@ flat out uvec2 f_id; flat out vec2 f_uv_minmax; out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; uniform vec2 resolution; uniform float pattern_length; @@ -56,7 +56,7 @@ void emit_vertex(vec3 position, vec2 uv, int index, float thickness) gl_Position = vec4((position.xy / resolution), position.z, 1.0); f_id = g_id[index]; // linewidth scaling may shrink the effective linewidth - f_thickness = thickness; + f_thickness = thickness; EmitVertex(); } // default for miter joins @@ -131,7 +131,7 @@ void emit_vertex(vec3 position, vec2 offset, vec2 line_dir, vec2 uv) // Generate line segment with 3 triangles // - p1, p2 are the line start and end points in pixel space -// - miter_a and miter_b are the offsets from p1 and p2 respectively that +// - miter_a and miter_b are the offsets from p1 and p2 respectively that // generate the line segment quad. This should include thickness and AA // - u1, u2 are the u values at p1 and p2. These should be in uv scale (px2uv applied) // - thickness_aa1, thickness_aa2 are linewidth at p1 and p2 with AA added. They @@ -162,7 +162,7 @@ void generate_line_segment( // \ / // \/ // /\ - // >--< + // >--< // Line segment has zero or negative width on short side // Pulled apart, we draw these two triangles (vertical lines added) @@ -171,9 +171,9 @@ void generate_line_segment( // X | | X // \| |/ // - // where X is u1/p1 (left) and u2/p2 (right) respectively. To avoid - // drawing outside the line segment due to AA padding, we cut off the - // left triangle on the right side at u2 via f_uv_minmax.y, and + // where X is u1/p1 (left) and u2/p2 (right) respectively. To avoid + // drawing outside the line segment due to AA padding, we cut off the + // left triangle on the right side at u2 via f_uv_minmax.y, and // analogously the right triangle at u1 via f_uv_minmax.x. // These triangles will still draw over each other like this. @@ -194,7 +194,7 @@ void generate_line_segment( // outgoing side f_uv_minmax.x = u1; f_uv_minmax.y = old; - + emit_vertex(p2, -miter_b, v1, vec2(u2, -thickness_aa2), 2); emit_vertex(p2, +miter_b, v1, vec2(u2, +thickness_aa2), 2); if (line_offset_b < 0){ // finish triangle on -miter_b side @@ -220,9 +220,9 @@ void generate_line_segment_debug( emit_vertex(p1 - vec3(miter_a, 0), vec2(u1 - px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(1, 0, 0, 0.5)); emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), -thickness_aa1), 1, vec4(1, 0, 0, 0.5)); emit_vertex(p2 - vec3(miter_b, 0), vec2(u2 - px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(1, 0, 0, 0.5)); - + EndPrimitive(); - + emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), -thickness_aa1), 1, vec4(0, 0, 1, 0.5)); emit_vertex(p2 - vec3(miter_b, 0), vec2(u2 - px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(0, 0, 1, 0.5)); emit_vertex(p2 + vec3(miter_b, 0), vec2(u2 + px2uv * dot(v1, miter_b), -thickness_aa2), 2, vec4(0, 0, 1, 0.5)); @@ -238,13 +238,13 @@ void generate_line_segment_debug( emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), -thickness_aa1), 1, vec4(1, 0, 0, 0.5)); emit_vertex(p1 - vec3(miter_a, 0), vec2(u1 - px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(1, 0, 0, 0.5)); emit_vertex(pc + vec3(miter_c, 0), vec2(uc + px2uv * dot(v1, miter_c), -thickness_aac), vec4(1, 0, 0, 0.5)); - + EndPrimitive(); - + emit_vertex(p1 - vec3(miter_a, 0), vec2(u1 - px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(0, 1, 0, 0.5)); emit_vertex(pc + vec3(miter_c, 0), vec2(uc + px2uv * dot(v1, miter_c), -thickness_aac), vec4(0, 1, 0, 0.5)); emit_vertex(p2 - vec3(miter_b, 0), vec2(u2 - px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(0, 1, 0, 0.5)); - + EndPrimitive(); emit_vertex(pc + vec3(miter_c, 0), vec2(uc + px2uv * dot(v1, miter_c), -thickness_aac), vec4(0, 0, 1, 0.5)); @@ -256,13 +256,13 @@ void generate_line_segment_debug( emit_vertex(p1 - vec3(miter_a, 0), vec2(u1 - px2uv * dot(v1, miter_a), -thickness_aa1), 1, vec4(1, 0, 0, 0.5)); emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(1, 0, 0, 0.5)); emit_vertex(pc - vec3(miter_c, 0), vec2(uc - px2uv * dot(v1, miter_c), -thickness_aac), vec4(1, 0, 0, 0.5)); - + EndPrimitive(); - + emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(0, 1, 0, 0.5)); emit_vertex(pc - vec3(miter_c, 0), vec2(uc - px2uv * dot(v1, miter_c), -thickness_aac), vec4(0, 1, 0, 0.5)); emit_vertex(p2 + vec3(miter_b, 0), vec2(u2 + px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(0, 1, 0, 0.5)); - + EndPrimitive(); emit_vertex(pc - vec3(miter_c, 0), vec2(uc - px2uv * dot(v1, miter_c), -thickness_aac), vec4(0, 0, 1, 0.5)); @@ -279,14 +279,14 @@ void generate_line_segment_debug( emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(1, 0, 0, 0.5)); if (line_offset_a > 0){ // finish triangle on -miter_a side emit_vertex( - p1 + vec3(2 * line_offset_a * v1 - miter_a, 0), - vec2(u1 + px2uv * (2 * line_offset_a - dot(v1, miter_a)), -thickness_aa1), + p1 + vec3(2 * line_offset_a * v1 - miter_a, 0), + vec2(u1 + px2uv * (2 * line_offset_a - dot(v1, miter_a)), -thickness_aa1), 1, vec4(1, 0, 0, 0.5) ); } else { emit_vertex( - p1 + vec3(-2 * line_offset_a * v1 + miter_a, 0), - vec2(u1 + px2uv * (-2 * line_offset_a + dot(v1, miter_a)), thickness_aa1), + p1 + vec3(-2 * line_offset_a * v1 + miter_a, 0), + vec2(u1 + px2uv * (-2 * line_offset_a + dot(v1, miter_a)), thickness_aa1), 1, vec4(1, 0, 0, 0.5) ); } @@ -300,14 +300,14 @@ void generate_line_segment_debug( emit_vertex(p2 + vec3(miter_b, 0), vec2(u2 + px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(0, 0, 1, 0.5)); if (line_offset_b < 0){ // finish triangle on -miter_b side emit_vertex( - p2 + vec3(2 * line_offset_b * v1 - miter_b, 0), - vec2(u2 + px2uv * (2 * line_offset_b - dot(v1, miter_b)), -thickness_aa2), + p2 + vec3(2 * line_offset_b * v1 - miter_b, 0), + vec2(u2 + px2uv * (2 * line_offset_b - dot(v1, miter_b)), -thickness_aa2), 2, vec4(0, 0, 1, 0.5) ); } else { emit_vertex( - p2 + vec3(-2 * line_offset_b * v1 + miter_b, 0), - vec2(u2 + px2uv * (-2 * line_offset_b + dot(v1, miter_b)), thickness_aa2), + p2 + vec3(-2 * line_offset_b * v1 + miter_b, 0), + vec2(u2 + px2uv * (-2 * line_offset_b + dot(v1, miter_b)), thickness_aa2), 2, vec4(0, 0, 1, 0.5) ); } @@ -601,8 +601,8 @@ void draw_patterned_line(bool isvalid[4]) // Debug - show each triangle // generate_line_segment_debug( - // p1, miter_a, start, thickness_aa1, - // p2, miter_b, stop, thickness_aa2, + // p1, miter_a, start, thickness_aa1, + // p2, miter_b, stop, thickness_aa2, // v1.xy, segment_length // ); @@ -773,7 +773,7 @@ void main(void) { // These need to be set but don't have reasonable values here o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); // we generate very thin lines for linewidth 0, so we manually skip them: if (g_thickness[1] == 0.0 && g_thickness[2] == 0.0) { diff --git a/GLMakie/assets/shader/lines.vert b/GLMakie/assets/shader/lines.vert index b5f648c8bb2..d789fdbd04f 100644 --- a/GLMakie/assets/shader/lines.vert +++ b/GLMakie/assets/shader/lines.vert @@ -26,6 +26,7 @@ vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_ uniform mat4 projectionview, model; uniform uint objectid; uniform int total_length; +uniform float px_per_unit; out uvec2 g_id; out vec4 g_color; @@ -50,7 +51,7 @@ void main() int index = gl_VertexID; g_id = uvec2(objectid, index+1); g_valid_vertex = get_valid_vertex(valid_vertex); - g_thickness = thickness; + g_thickness = px_per_unit * thickness; g_color = _color(color, intensity, color_map, color_norm, index, total_length); #ifdef FAST_PATH diff --git a/GLMakie/assets/shader/mesh.frag b/GLMakie/assets/shader/mesh.frag index 26df220316d..5480da20008 100644 --- a/GLMakie/assets/shader/mesh.frag +++ b/GLMakie/assets/shader/mesh.frag @@ -4,15 +4,11 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d bool _; //empty structs are not allowed }; -uniform vec3 ambient; -uniform vec3 diffuse; -uniform vec3 specular; -uniform float shininess; -uniform float backlight; +// Sets which shading procedures to use +{{shading}} -in vec3 o_normal; -in vec3 o_lightdir; -in vec3 o_camdir; +in vec3 o_world_normal; +in vec3 o_view_normal; in vec4 o_color; in vec2 o_uv; flat in uvec2 o_id; @@ -68,7 +64,8 @@ vec4 get_color(sampler2D intensity, vec2 uv, vec2 color_norm, sampler1D color_ma return get_color_from_cmap(i, color_map, color_norm); } vec4 matcap_color(sampler2D matcap){ - vec2 muv = o_normal.xy * 0.5 + vec2(0.5, 0.5); + // TODO should matcaps use view space normals? + vec2 muv = o_view_normal.xy * 0.5 + vec2(0.5, 0.5); return texture(matcap, vec2(1.0-muv.y, muv.x)); } vec4 get_color(Nothing image, vec2 uv, Nothing color_norm, Nothing color_map, sampler2D matcap){ @@ -100,25 +97,11 @@ vec4 get_pattern_color(sampler2D color){ // Needs to exist for opengl to be happy vec4 get_pattern_color(Nothing color){return vec4(1,0,1,1);} -vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0); - - // specular coefficient - vec3 H = normalize(L + V); - - float spec_coeff = pow(max(dot(H, N), 0.0), shininess); - if (diff_coeff <= 0.0 || isnan(spec_coeff)) - spec_coeff = 0.0; - - // final lighting model - return vec3( - diffuse * diff_coeff * color + - specular * spec_coeff - ); -} - void write2framebuffer(vec4 color, uvec2 id); +#ifndef NO_SHADING +vec3 illuminate(vec3 normal, vec3 base_color); +#endif void main(){ vec4 color; @@ -128,6 +111,8 @@ void main(){ }else{ color = get_color(image, o_uv, color_norm, color_map, matcap); } - {{light_calc}} + #ifndef NO_SHADING + color.rgb = illuminate(normalize(o_world_normal), color.rgb); + #endif write2framebuffer(color, o_id); } diff --git a/GLMakie/assets/shader/mesh.vert b/GLMakie/assets/shader/mesh.vert index 75d3f0d8b3a..018248c0c5c 100644 --- a/GLMakie/assets/shader/mesh.vert +++ b/GLMakie/assets/shader/mesh.vert @@ -14,10 +14,9 @@ uniform bool interpolate_in_fragment_shader = false; in vec3 normals; -uniform vec3 lightposition; uniform mat4 projection, view, model; -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition); +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); vec4 get_color_from_cmap(float value, sampler1D color_map, vec2 colorrange); uniform uint objectid; @@ -63,5 +62,5 @@ void main() o_uv = vec2(1.0 - tex_uv.y, tex_uv.x) * uv_scale; o_color = to_color(vertex_color, color_map, color_norm); vec3 v = to_3d(vertices); - render(model * vec4(v, 1), normals, view, projection, lightposition); + render(model * vec4(v, 1), normals, view, projection); } diff --git a/GLMakie/assets/shader/particles.vert b/GLMakie/assets/shader/particles.vert index 8676a2a682f..2d26785e54d 100644 --- a/GLMakie/assets/shader/particles.vert +++ b/GLMakie/assets/shader/particles.vert @@ -28,7 +28,6 @@ in vec3 vertices; in vec3 normals; {{texturecoordinates_type}} texturecoordinates; -uniform vec3 lightposition; uniform mat4 view, model, projection; uniform uint objectid; uniform int len; @@ -91,7 +90,7 @@ vec4 get_particle_color(sampler2D color, Nothing intensity, Nothing color_map, N return vec4(0); } -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition); +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); vec2 get_uv(Nothing x){return vec2(0.0);} vec2 get_uv(vec2 x){return vec2(1.0 - x.y, x.x);} @@ -108,5 +107,5 @@ void main(){ o_color = o_color * to_color(vertex_color); o_uv = get_uv(texturecoordinates); rotate(rotation, index, V, N); - render(model * vec4(pos + V, 1), N, view, projection, lightposition); + render(model * vec4(pos + V, 1), N, view, projection); } diff --git a/GLMakie/assets/shader/sprites.geom b/GLMakie/assets/shader/sprites.geom index d34d131a187..fc771ef63b5 100644 --- a/GLMakie/assets/shader/sprites.geom +++ b/GLMakie/assets/shader/sprites.geom @@ -85,7 +85,6 @@ void emit_vertex(vec4 vertex, vec2 uv) f_uv_texture_bbox = g_uv_texture_bbox[0]; f_primitive_index = g_primitive_index[0]; f_color = g_color[0]; - f_bg_color = vec4(g_color[0].rgb, 0); f_stroke_color = g_stroke_color[0]; f_glow_color = g_glow_color[0]; f_id = g_id[0]; @@ -99,12 +98,12 @@ mat2 diagm(vec2 v){ } out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; void main(void) { o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); // emit quad as triangle strip // v3. ____ . v4 diff --git a/GLMakie/assets/shader/sprites.vert b/GLMakie/assets/shader/sprites.vert index 2489dddd040..ceee24efe7f 100644 --- a/GLMakie/assets/shader/sprites.vert +++ b/GLMakie/assets/shader/sprites.vert @@ -72,6 +72,7 @@ vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_ {{stroke_color_type}} stroke_color; {{glow_color_type}} glow_color; +uniform bool scale_primitive; uniform mat4 preprojection; uniform mat4 model; uniform uint objectid; @@ -96,7 +97,10 @@ void main(){ vec3 pos; {{position_calc}} vec4 p = preprojection * model * vec4(pos, 1); - g_position = p.xyz / p.w + mat3(model) * marker_offset; + if (scale_primitive) + g_position = p.xyz / p.w + mat3(model) * marker_offset; + else + g_position = p.xyz / p.w + marker_offset; g_offset_width.xy = quad_offset.xy; g_offset_width.zw = scale.xy; g_color = _color(color, intensity, color_map, color_norm, g_primitive_index, len); diff --git a/GLMakie/assets/shader/surface.vert b/GLMakie/assets/shader/surface.vert index 0388819e0e1..b8b16eb98d6 100644 --- a/GLMakie/assets/shader/surface.vert +++ b/GLMakie/assets/shader/surface.vert @@ -19,8 +19,6 @@ in vec2 vertices; {{position_y_type}} position_y; uniform sampler2D position_z; -uniform vec3 lightposition; - {{image_type}} image; {{color_map_type}} color_map; {{color_norm_type}} color_norm; @@ -36,16 +34,117 @@ uniform vec3 scale; uniform mat4 view, model, projection; // See util.vert for implementations -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition); +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); ivec2 ind2sub(ivec2 dim, int linearindex); vec2 grid_pos(Grid2D pos, vec2 uv); vec2 linear_index(ivec2 dims, int index); vec2 linear_index(ivec2 dims, int index, vec2 offset); vec4 linear_texture(sampler2D tex, int index, vec2 offset); -// vec3 getnormal_fast(sampler2D zvalues, ivec2 uv); -vec3 getnormal(Grid2D pos, Nothing xs, Nothing ys, sampler2D zs, vec2 uv); -vec3 getnormal(Nothing pos, sampler2D xs, sampler2D ys, sampler2D zs, vec2 uv); -vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, vec2 uv); + + +// Normal generation + +vec3 getnormal_fast(sampler2D zvalues, ivec2 uv) +{ + vec3 a = vec3(0, 0, 0); + vec3 b = vec3(1, 1, 0); + a.z = texelFetch(zvalues, uv, 0).r; + b.z = texelFetch(zvalues, uv + ivec2(1, 1), 0).r; + return normalize(a - b); +} + +bool isinbounds(ivec2 uv, ivec2 size) +{ + return (uv.x < size.x && uv.y < size.y && uv.x >= 0 && uv.y >= 0); +} + +/* +Computes normal at s0 based on four surrounding positions s1 ... s4 and the +respective uv coordinates uv, off1, ..., off4 + + s2 + s1 s0 s3 + s4 +*/ +vec3 normal_from_points( + vec3 s0, vec3 s1, vec3 s2, vec3 s3, vec3 s4, + ivec2 off1, ivec2 off2, ivec2 off3, ivec2 off4, ivec2 size + ){ + vec3 result = vec3(0,0,0); + // isnan checks should avoid darkening around NaN positions but may not + // work with all systems + if (!isnan(s0.z)) { + bool check1 = isinbounds(off1, size) && !isnan(s1.z); + bool check2 = isinbounds(off2, size) && !isnan(s2.z); + bool check3 = isinbounds(off3, size) && !isnan(s3.z); + bool check4 = isinbounds(off4, size) && !isnan(s4.z); + if (check1 && check2) result += cross(s2-s0, s1-s0); + if (check2 && check3) result += cross(s3-s0, s2-s0); + if (check3 && check4) result += cross(s4-s0, s3-s0); + if (check4 && check1) result += cross(s1-s0, s4-s0); + } + // normal should be zero, but needs to be here, because the dead-code + // elimanation of GLSL is overly enthusiastic + return normalize(result); +} + +// Overload for surface(Matrix, Matrix, Matrix) +vec3 getnormal(Nothing pos, sampler2D xs, sampler2D ys, sampler2D zs, ivec2 uv){ + vec3 s0, s1, s2, s3, s4; + ivec2 off1 = uv + ivec2(-1, 0); + ivec2 off2 = uv + ivec2(0, 1); + ivec2 off3 = uv + ivec2(1, 0); + ivec2 off4 = uv + ivec2(0, -1); + + s0 = vec3(texelFetch(xs, uv, 0).x, texelFetch(ys, uv, 0).x, texelFetch(zs, uv, 0).x); + s1 = vec3(texelFetch(xs, off1, 0).x, texelFetch(ys, off1, 0).x, texelFetch(zs, off1, 0).x); + s2 = vec3(texelFetch(xs, off2, 0).x, texelFetch(ys, off2, 0).x, texelFetch(zs, off2, 0).x); + s3 = vec3(texelFetch(xs, off3, 0).x, texelFetch(ys, off3, 0).x, texelFetch(zs, off3, 0).x); + s4 = vec3(texelFetch(xs, off4, 0).x, texelFetch(ys, off4, 0).x, texelFetch(zs, off4, 0).x); + + return normal_from_points(s0, s1, s2, s3, s4, off1, off2, off3, off4, textureSize(zs, 0)); +} + + +// Overload for (range, range, Matrix) surface plots +// Though this is only called by surface(Matrix) +vec2 grid_pos(Grid2D position, ivec2 uv, ivec2 size); + +vec3 getnormal(Grid2D pos, Nothing xs, Nothing ys, sampler2D zs, ivec2 uv){ + vec3 s0, s1, s2, s3, s4; + ivec2 off1 = uv + ivec2(-1, 0); + ivec2 off2 = uv + ivec2(0, 1); + ivec2 off3 = uv + ivec2(1, 0); + ivec2 off4 = uv + ivec2(0, -1); + ivec2 size = textureSize(zs, 0); + + s0 = vec3(grid_pos(pos, uv, size).xy, texelFetch(zs, uv, 0).x); + s1 = vec3(grid_pos(pos, off1, size).xy, texelFetch(zs, off1, 0).x); + s2 = vec3(grid_pos(pos, off2, size).xy, texelFetch(zs, off2, 0).x); + s3 = vec3(grid_pos(pos, off3, size).xy, texelFetch(zs, off3, 0).x); + s4 = vec3(grid_pos(pos, off4, size).xy, texelFetch(zs, off4, 0).x); + + return normal_from_points(s0, s1, s2, s3, s4, off1, off2, off3, off4, size); +} + + +// Overload for surface(Vector, Vector, Matrix) +// Makie converts almost everything to this +vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, ivec2 uv){ + vec3 s0, s1, s2, s3, s4; + ivec2 off1 = uv + ivec2(-1, 0); + ivec2 off2 = uv + ivec2(0, 1); + ivec2 off3 = uv + ivec2(1, 0); + ivec2 off4 = uv + ivec2(0, -1); + + s0 = vec3(texelFetch(xs, uv.x, 0).x, texelFetch(ys, uv.y, 0).x, texelFetch(zs, uv, 0).x); + s1 = vec3(texelFetch(xs, off1.x, 0).x, texelFetch(ys, off1.y, 0).x, texelFetch(zs, off1, 0).x); + s2 = vec3(texelFetch(xs, off2.x, 0).x, texelFetch(ys, off2.y, 0).x, texelFetch(zs, off2, 0).x); + s3 = vec3(texelFetch(xs, off3.x, 0).x, texelFetch(ys, off3.y, 0).x, texelFetch(zs, off3, 0).x); + s4 = vec3(texelFetch(xs, off4.x, 0).x, texelFetch(ys, off4.y, 0).x, texelFetch(zs, off4, 0).x); + + return normal_from_points(s0, s1, s2, s3, s4, off1, off2, off3, off4, textureSize(zs, 0)); +} uniform uint objectid; uniform vec2 uv_scale; @@ -71,5 +170,5 @@ void main() if (isnan(pos.z)) { pos.z = 0.0; } - render(model * vec4(pos, 1), normalvec, view, projection, lightposition); + render(model * vec4(pos, 1), normalvec, view, projection); } diff --git a/GLMakie/assets/shader/util.vert b/GLMakie/assets/shader/util.vert index 5d3863deff8..afb2c379945 100644 --- a/GLMakie/assets/shader/util.vert +++ b/GLMakie/assets/shader/util.vert @@ -1,5 +1,12 @@ {{GLSL_VERSION}} +// Sets which shading procedures to use +// Options: +// NO_SHADING - skip shading calculation, handled outside +// FAST_SHADING - single point light (forward rendering) +// MULTI_LIGHT_SHADING - simple shading with multiple lights (forward rendering) +{{shading}} + struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data bool _; //empty structs are not allowed }; @@ -29,6 +36,12 @@ vec2 grid_pos(Grid2D position, vec2 uv){ ); } +vec2 grid_pos(Grid2D position, ivec2 uv, ivec2 size){ + return vec2( + (1.0 - (uv.x + 0.5) / size.x) * position.start[0] + (uv.x + 0.5) / size.x * position.stop[0], + (1.0 - (uv.y + 0.5) / size.y) * position.start[1] + (uv.y + 0.5) / size.y * position.stop[1] + ); +} // stretch is vec3 stretch(vec3 val, vec3 from, vec3 to){ @@ -117,25 +130,25 @@ uniform vec4 lowclip; uniform vec4 nan_color; vec4 get_color_from_cmap(float value, sampler1D color_map, vec2 colorrange) { -float cmin = colorrange.x; -float cmax = colorrange.y; -if (value <= cmax && value >= cmin) { - // in value range, continue! -} else if (value < cmin) { -return lowclip; -} else if (value > cmax) { -return highclip; -} else { - // isnan CAN be broken (of course) -.- - // so if outside value range and not smaller/bigger min/max we assume NaN -return nan_color; -} -float i01 = clamp((value - cmin) / (cmax - cmin), 0.0, 1.0); - // 1/0 corresponds to the corner of the colormap, so to properly interpolate - // between the colors, we need to scale it, so that the ends are at 1 - (stepsize/2) and 0+(stepsize/2). -float stepsize = 1.0 / float(textureSize(color_map, 0)); -i01 = (1.0 - stepsize) * i01 + 0.5 * stepsize; -return texture(color_map, i01); + float cmin = colorrange.x; + float cmax = colorrange.y; + if (value <= cmax && value >= cmin) { + // in value range, continue! + } else if (value < cmin) { + return lowclip; + } else if (value > cmax) { + return highclip; + } else { + // isnan CAN be broken (of course) -.- + // so if outside value range and not smaller/bigger min/max we assume NaN + return nan_color; + } + float i01 = clamp((value - cmin) / (cmax - cmin), 0.0, 1.0); + // 1/0 corresponds to the corner of the colormap, so to properly interpolate + // between the colors, we need to scale it, so that the ends are at 1 - (stepsize/2) and 0+(stepsize/2). + float stepsize = 1.0 / float(textureSize(color_map, 0)); + i01 = (1.0 - stepsize) * i01 + 0.5 * stepsize; + return texture(color_map, i01); } @@ -235,150 +248,47 @@ vec4 _color(Nothing color, float intensity, sampler1D color_map, vec2 color_norm return get_color_from_cmap(intensity, color_map, color_norm); } -out vec3 o_view_pos; -out vec3 o_normal; -out vec3 o_lightdir; -out vec3 o_camdir; + +uniform float depth_shift; + +// TODO maybe ifdef SSAO this stuff? // transpose(inv(view * model)) // Transformation for vectors (rather than points) -uniform mat3 normalmatrix; -uniform vec3 lightposition; +uniform mat3 view_normalmatrix; +out vec3 o_view_pos; +out vec3 o_view_normal; + + +#if defined(FAST_SHADING) || defined(MULTI_LIGHT_SHADING) +// transpose(inv(model)) +uniform mat3 world_normalmatrix; uniform vec3 eyeposition; -uniform float depth_shift; +out vec3 o_world_pos; +out vec3 o_world_normal; +out vec3 o_camdir; +#endif -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition) +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection) { - // normal in world space - o_normal = normalmatrix * normal; // position in view space (as seen from camera) vec4 view_pos = view * position_world; + view_pos /= view_pos.w; + // position in clip space (w/ depth) gl_Position = projection * view_pos; gl_Position.z += gl_Position.w * depth_shift; - // direction to light - o_lightdir = normalize(view*vec4(lightposition, 1.0) - view_pos).xyz; - // direction to camera - // This is equivalent to - // normalize(view*vec4(eyeposition, 1.0) - view_pos).xyz - // (by definition `view * eyeposition = 0`) - o_camdir = normalize(-view_pos).xyz; - o_view_pos = view_pos.xyz / view_pos.w; -} -// -vec3 getnormal_fast(sampler2D zvalues, ivec2 uv) -{ - vec3 a = vec3(0, 0, 0); - vec3 b = vec3(1, 1, 0); - a.z = texelFetch(zvalues, uv, 0).r; - b.z = texelFetch(zvalues, uv + ivec2(1, 1), 0).r; - return normalize(a - b); -} + // for lighting +#if defined(FAST_SHADING) || defined(MULTI_LIGHT_SHADING) + o_world_pos = position_world.xyz / position_world.w; + o_world_normal = world_normalmatrix * normal; + // direction from camera to vertex + o_camdir = position_world.xyz / position_world.w - eyeposition; +#endif -bool isinbounds(vec2 uv) -{ - return (uv.x <= 1.0 && uv.y <= 1.0 && uv.x >= 0.0 && uv.y >= 0.0); -} - - -/* -Computes normal at s0 based on four surrounding positions s1 ... s4 and the -respective uv coordinates uv, off1, ..., off4 - - s2 - s1 s0 s3 - s4 -*/ -vec3 normal_from_points( - vec3 s0, vec3 s1, vec3 s2, vec3 s3, vec3 s4, - vec2 uv, vec2 off1, vec2 off2, vec2 off3, vec2 off4 - ){ - vec3 result = vec3(0); - if(isinbounds(off1) && isinbounds(off2)) - { - result += cross(s2-s0, s1-s0); - } - if(isinbounds(off2) && isinbounds(off3)) - { - result += cross(s3-s0, s2-s0); - } - if(isinbounds(off3) && isinbounds(off4)) - { - result += cross(s4-s0, s3-s0); - } - if(isinbounds(off4) && isinbounds(off1)) - { - result += cross(s1-s0, s4-s0); - } - // normal should be zero, but needs to be here, because the dead-code - // elimanation of GLSL is overly enthusiastic - return normalize(result); -} - -// Overload for surface(Matrix, Matrix, Matrix) -vec3 getnormal(Nothing pos, sampler2D xs, sampler2D ys, sampler2D zs, vec2 uv){ - // The +1e-6 fixes precision errors at the edge - float du = 1.0 / textureSize(zs,0).x + 1e-6; - float dv = 1.0 / textureSize(zs,0).y + 1e-6; - - vec3 s0, s1, s2, s3, s4; - vec2 off1 = uv + vec2(-du, 0); - vec2 off2 = uv + vec2(0, dv); - vec2 off3 = uv + vec2(du, 0); - vec2 off4 = uv + vec2(0, -dv); - - s0 = vec3(texture(xs, uv).x, texture(ys, uv).x, texture(zs, uv).x); - s1 = vec3(texture(xs, off1).x, texture(ys, off1).x, texture(zs, off1).x); - s2 = vec3(texture(xs, off2).x, texture(ys, off2).x, texture(zs, off2).x); - s3 = vec3(texture(xs, off3).x, texture(ys, off3).x, texture(zs, off3).x); - s4 = vec3(texture(xs, off4).x, texture(ys, off4).x, texture(zs, off4).x); - - return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4); -} - - -// Overload for (range, range, Matrix) surface plots -// Though this is only called by surface(Matrix) -vec3 getnormal(Grid2D pos, Nothing xs, Nothing ys, sampler2D zs, vec2 uv){ - // The +1e-6 fixes precision errors at the edge - float du = 1.0 / textureSize(zs,0).x + 1e-6; - float dv = 1.0 / textureSize(zs,0).y + 1e-6; - - vec3 s0, s1, s2, s3, s4; - vec2 off1 = uv + vec2(-du, 0); - vec2 off2 = uv + vec2(0, dv); - vec2 off3 = uv + vec2(du, 0); - vec2 off4 = uv + vec2(0, -dv); - - s0 = vec3(grid_pos(pos, uv).xy, texture(zs, uv).x); - s1 = vec3(grid_pos(pos, off1).xy, texture(zs, off1).x); - s2 = vec3(grid_pos(pos, off2).xy, texture(zs, off2).x); - s3 = vec3(grid_pos(pos, off3).xy, texture(zs, off3).x); - s4 = vec3(grid_pos(pos, off4).xy, texture(zs, off4).x); - - return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4); -} - - -// Overload for surface(Vector, Vector, Matrix) -// Makie converts almost everything to this -vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, vec2 uv){ - // The +1e-6 fixes precision errors at the edge - float du = 1.0 / textureSize(zs,0).x + 1e-6; - float dv = 1.0 / textureSize(zs,0).y + 1e-6; - - vec3 s0, s1, s2, s3, s4; - vec2 off1 = uv + vec2(-du, 0); - vec2 off2 = uv + vec2(0, dv); - vec2 off3 = uv + vec2(du, 0); - vec2 off4 = uv + vec2(0, -dv); - - s0 = vec3(texture(xs, uv.x).x, texture(ys, uv.y).x, texture(zs, uv).x); - s1 = vec3(texture(xs, off1.x).x, texture(ys, off1.y).x, texture(zs, off1).x); - s2 = vec3(texture(xs, off2.x).x, texture(ys, off2.y).x, texture(zs, off2).x); - s3 = vec3(texture(xs, off3.x).x, texture(ys, off3.y).x, texture(zs, off3).x); - s4 = vec3(texture(xs, off4.x).x, texture(ys, off4.y).x, texture(zs, off4).x); - - return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4); + // for SSAO + o_view_pos = view_pos.xyz / view_pos.w; + // SSAO + matcap + o_view_normal = view_normalmatrix * normal; } diff --git a/GLMakie/assets/shader/volume.frag b/GLMakie/assets/shader/volume.frag index 4fdcadf3d25..1e4eef28b6d 100644 --- a/GLMakie/assets/shader/volume.frag +++ b/GLMakie/assets/shader/volume.frag @@ -1,10 +1,16 @@ {{GLSL_VERSION}} +// Sets which shading procedures to use +// Options: +// NO_SHADING - skip shading calculation, handled outside +// FAST_SHADING - single point light (forward rendering) +// MULTI_LIGHT_SHADING - simple shading with multiple lights (forward rendering) +{{shading}} + struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data bool _; //empty structs are not allowed }; in vec3 frag_vert; -in vec3 o_light_dir; {{volumedata_type}} volumedata; @@ -15,11 +21,6 @@ in vec3 o_light_dir; uniform float absorption = 1.0; uniform vec3 eyeposition; -uniform vec3 ambient; -uniform vec3 diffuse; -uniform vec3 specular; -uniform float shininess; - uniform mat4 modelinv; uniform int algorithm; uniform float isovalue; @@ -72,9 +73,17 @@ vec4 color_lookup(Nothing colormap, int index) return vec4(0); } -vec3 gennormal(vec3 uvw, float d) +vec3 gennormal(vec3 uvw, float d, vec3 o) { + // uvw samples positions (0..1 values) + // d is the sampling step. Could be any small value here + // o is half the uvw distance between two voxels. A distance smaller than + // that will result in equal positions when sampling on the edge of the + // volume, generating broken normals. vec3 a, b; + + float eps = 0.001; + // handle normals at edges! if(uvw.x + d >= 1.0){ return vec3(1, 0, 0); @@ -96,32 +105,33 @@ vec3 gennormal(vec3 uvw, float d) return vec3(0, 0, -1); } - a.x = texture(volumedata, uvw - vec3(d,0.0,0.0)).r; - b.x = texture(volumedata, uvw + vec3(d,0.0,0.0)).r; + a.x = texture(volumedata, uvw - vec3(o.x, 0.0, 0.0)).r; + b.x = texture(volumedata, uvw + vec3(o.x, 0.0, 0.0)).r; - a.y = texture(volumedata, uvw - vec3(0.0,d,0.0)).r; - b.y = texture(volumedata, uvw + vec3(0.0,d,0.0)).r; + a.y = texture(volumedata, uvw - vec3(0.0, o.y, 0.0)).r; + b.y = texture(volumedata, uvw + vec3(0.0, o.y, 0.0)).r; - a.z = texture(volumedata, uvw - vec3(0.0,0.0,d)).r; - b.z = texture(volumedata, uvw + vec3(0.0,0.0,d)).r; - return normalize(a-b); + a.z = texture(volumedata, uvw - vec3(0.0, 0.0, o.z)).r; + b.z = texture(volumedata, uvw + vec3(0.0, 0.0, o.z)).r; + + vec3 diff = a - b; + float n = length(diff); + + if (n < 0.000000000001) // 1e-12 + return diff; + + return diff / n; } -// Includes front and back-facing normals (N, -N) -vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0) + max(dot(L, -N), 0.0); - // specular coefficient - vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0) + max(dot(H, -N), 0.0), shininess); - if (diff_coeff <= 0.0 || isnan(spec_coeff)) - spec_coeff = 0.0; - // final lighting model - return vec3( - ambient * color + - diffuse * diff_coeff * color + - specular * spec_coeff - ); +#ifndef NO_SHADING +vec3 illuminate(vec3 world_pos, vec3 camdir, vec3 normal, vec3 base_color); +#endif + +#ifdef NO_SHADING +vec3 illuminate(vec3 world_pos, vec3 camdir, vec3 normal, vec3 base_color) { + return normal; } +#endif // Simple random generator found: http://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl float rand(){ @@ -208,7 +218,8 @@ vec4 contours(vec3 front, vec3 dir) float T = 1.0; vec3 Lo = vec3(0.0); int i = 0; - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); + vec3 edge_gap = 0.5 / textureSize(volumedata, 0); // see gennormal {{depth_init}} // may write: float depth = 100000.0; for (i; i < num_samples; ++i) { @@ -220,9 +231,9 @@ vec4 contours(vec3 front, vec3 dir) // may write // vec4 frag_coord = projectionview * model * vec4(pos, 1); // depth = min(depth, frag_coord.z / frag_coord.w); - vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); - vec3 opaque = blinnphong(N, camdir, L, density.rgb); + vec3 N = gennormal(pos, step_size, edge_gap); + vec4 world_pos = model * vec4(pos, 1); + vec3 opaque = illuminate(world_pos.xyz / world_pos.w, camdir, N, density.rgb); Lo += (T * opacity) * opaque; T *= 1.0 - opacity; if (T <= 0.01) @@ -242,7 +253,8 @@ vec4 isosurface(vec3 front, vec3 dir) vec4 c = vec4(0.0); int i = 0; vec4 diffuse_color = color_lookup(isovalue, color_map, color_norm, color); - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); + vec3 edge_gap = 0.5 / textureSize(volumedata, 0); // see gennormal {{depth_init}} // may write: float depth = 100000.0; for (i; i < num_samples; ++i){ @@ -252,10 +264,10 @@ vec4 isosurface(vec3 front, vec3 dir) // may write: // vec4 frag_coord = projectionview * model * vec4(pos, 1); // depth = min(depth, frag_coord.z / frag_coord.w); - vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); + vec3 N = gennormal(pos, step_size, edge_gap); + vec4 world_pos = model * vec4(pos, 1); c = vec4( - blinnphong(N, camdir, L, diffuse_color.rgb), + illuminate(world_pos.xyz / world_pos.w, camdir, N, diffuse_color.rgb), diffuse_color.a ); break; diff --git a/GLMakie/assets/shader/volume.vert b/GLMakie/assets/shader/volume.vert index 72c60a336c8..2ca396b5763 100644 --- a/GLMakie/assets/shader/volume.vert +++ b/GLMakie/assets/shader/volume.vert @@ -3,24 +3,26 @@ in vec3 vertices; out vec3 frag_vert; -out vec3 o_light_dir; uniform mat4 projectionview, model; -uniform vec3 lightposition; uniform mat4 modelinv; uniform float depth_shift; +// SSAO out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; + +// Lighting (unused and don't need to be available?) +// out vec3 o_world_pos; +// out vec3 o_world_normal; void main() { // TODO set these in volume.frag o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); vec4 world_vert = model * vec4(vertices, 1); frag_vert = world_vert.xyz; - o_light_dir = vec3(modelinv * vec4(lightposition, 1)); gl_Position = projectionview * world_vert; gl_Position.z += gl_Position.w * depth_shift; } diff --git a/GLMakie/src/GLAbstraction/AbstractGPUArray.jl b/GLMakie/src/GLAbstraction/AbstractGPUArray.jl index 48da57bfc8a..17b39705e4f 100644 --- a/GLMakie/src/GLAbstraction/AbstractGPUArray.jl +++ b/GLMakie/src/GLAbstraction/AbstractGPUArray.jl @@ -193,12 +193,11 @@ max_dim(t) = error("max_dim not implemented for: $(typeof(t)). This happen function (::Type{GPUArrayType})(data::Observable; kw...) where GPUArrayType <: GPUArray gpu_mem = GPUArrayType(data[]; kw...) # TODO merge these and handle update tracking during contruction - obs1 = on(_-> gpu_mem.requires_update[] = true, data) obs2 = on(new_data -> update!(gpu_mem, new_data), data) if GPUArrayType <: TextureBuffer - push!(gpu_mem.buffer.observers, obs1, obs2) + push!(gpu_mem.buffer.observers, obs2) else - push!(gpu_mem.observers, obs1, obs2) + push!(gpu_mem.observers, obs2) end return gpu_mem end diff --git a/GLMakie/src/GLAbstraction/GLBuffer.jl b/GLMakie/src/GLAbstraction/GLBuffer.jl index 6f123ade0e4..a19d789af23 100644 --- a/GLMakie/src/GLAbstraction/GLBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLBuffer.jl @@ -5,7 +5,6 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1} usage::GLenum context::GLContext # TODO maybe also delay upload to when render happens? - requires_update::Observable{Bool} observers::Vector{Observables.ObserverFunction} function GLBuffer{T}(ptr::Ptr{T}, buff_length::Int, buffertype::GLenum, usage::GLenum) where T @@ -18,8 +17,7 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1} obj = new( id, (buff_length,), buffertype, usage, current_context(), - Observable(true), Observables.ObserverFunction[]) - + Observables.ObserverFunction[]) finalizer(free, obj) obj end @@ -68,7 +66,6 @@ function GLBuffer( au = ShaderAbstractions.updater(buffer) obsfunc = on(au.update) do (f, args) f(b, args...) # forward setindex! etc - b.requires_update[] = true return end push!(b.observers, obsfunc) diff --git a/GLMakie/src/GLAbstraction/GLRender.jl b/GLMakie/src/GLAbstraction/GLRender.jl index e48d3a11c3a..d6eb089f410 100644 --- a/GLMakie/src/GLAbstraction/GLRender.jl +++ b/GLMakie/src/GLAbstraction/GLRender.jl @@ -55,8 +55,6 @@ So rewriting this function could get us a lot of performance for scenes with a lot of objects. """ function render(renderobject::RenderObject, vertexarray=renderobject.vertexarray) - renderobject.requires_update = false - if renderobject.visible renderobject.prerenderfunction() program = vertexarray.program diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl index edbc7efff8c..bddc6e61302 100644 --- a/GLMakie/src/GLAbstraction/GLShader.jl +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -249,7 +249,7 @@ function gl_convert(cache::ShaderCache, lazyshader::AbstractLazyShader, data) template_keys[i] = template replacements[i] = String[mustache2replacement(t, v, data) for t in template] end - program = get!(cache.program_cache, (paths, replacements)) do + return get!(cache.program_cache, (paths, replacements)) do # when we're here, this means there were uncached shaders, meaning we definitely have # to compile a new program shaders = Vector{Shader}(undef, length(paths)) diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index 9f6cbd529d8..028c23db012 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -17,7 +17,6 @@ mutable struct Texture{T <: GLArrayEltypes, NDIM} <: OpenglTexture{T, NDIM} parameters ::TextureParameters{NDIM} size ::NTuple{NDIM, Int} context ::GLContext - requires_update ::Observable{Bool} observers ::Vector{Observables.ObserverFunction} function Texture{T, NDIM}( id ::GLuint, @@ -37,7 +36,6 @@ mutable struct Texture{T <: GLArrayEltypes, NDIM} <: OpenglTexture{T, NDIM} parameters, size, current_context(), - Observable(true), Observables.ObserverFunction[] ) finalizer(free, tex) @@ -49,11 +47,8 @@ end mutable struct TextureBuffer{T <: GLArrayEltypes} <: OpenglTexture{T, 1} texture::Texture{T, 1} buffer::GLBuffer{T} - requires_update::Observable{Bool} - function TextureBuffer(texture::Texture{T, 1}, buffer::GLBuffer{T}) where T - x = map((_, _) -> true, buffer.requires_update, texture.requires_update) - new{T}(texture, buffer, x) + new{T}(texture, buffer) end end Base.size(t::TextureBuffer) = size(t.buffer) @@ -72,7 +67,6 @@ ShaderAbstractions.switch_context!(t::TextureBuffer) = switch_context!(t.texture function unsafe_free(tb::TextureBuffer) unsafe_free(tb.texture) unsafe_free(tb.buffer) - Observables.clear(tb.requires_update) end is_texturearray(t::Texture) = t.texturetype == GL_TEXTURE_2D_ARRAY @@ -148,8 +142,7 @@ function Texture(s::ShaderAbstractions.Sampler{T, N}; kwargs...) where {T, N} anisotropic = s.anisotropic; kwargs... ) obsfunc = ShaderAbstractions.connect!(s, tex) - obsfunc2 = on(x -> tex.requires_update[] = true, s.updates.update) - push!(tex.observers, obsfunc, obsfunc2) + push!(tex.observers, obsfunc) return tex end diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 6e7a68f9c2a..19d7123e4fa 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -174,21 +174,8 @@ mutable struct GLVertexArray{T} buffers::Dict{String,GLBuffer} indices::T context::GLContext - requires_update::Observable{Bool} - function GLVertexArray{T}(program, id, bufferlength, buffers, indices) where T - va = new(program, id, bufferlength, buffers, indices, current_context(), true) - if indices isa GLBuffer - on(indices.requires_update) do _ # only triggers true anyway - va.requires_update[] = true - end - end - for (name, buffer) in buffers - on(buffer.requires_update) do _ # only triggers true anyway - va.requires_update[] = true - end - end - + va = new(program, id, bufferlength, buffers, indices, current_context()) return va end end @@ -318,7 +305,6 @@ mutable struct RenderObject{Pre} prerenderfunction::Pre postrenderfunction id::UInt32 - requires_update::Bool visible::Bool function RenderObject{Pre}( @@ -326,7 +312,7 @@ mutable struct RenderObject{Pre} uniforms::Dict{Symbol,Any}, observables::Vector{Observable}, vertexarray::GLVertexArray, prerenderfunctions, postrenderfunctions, - visible, track_updates = true + visible ) where Pre fxaa = Bool(to_value(get!(uniforms, :fxaa, true))) RENDER_OBJECT_ID_COUNTER[] += one(UInt32) @@ -340,57 +326,13 @@ mutable struct RenderObject{Pre} context, uniforms, observables, vertexarray, prerenderfunctions, postrenderfunctions, - id, true, visible[] + id, visible[] ) - - if track_updates - # visible changes should always trigger updates so that plots - # actually become invisible when visible is changed. - # Other uniforms and buffers don't need to trigger updates when - # visible = false - on(visible) do visible - robj.visible = visible - robj.requires_update = true - end - - function request_update(_::Any) - if robj.visible - robj.requires_update = true - end - return - end - - # gather update requests for polling in renderloop - for uniform in values(uniforms) - if uniform isa Observable - on(request_update, uniform) - elseif uniform isa GPUArray - on(request_update, uniform.requires_update) - end - end - on(request_update, vertexarray.requires_update) - else - on(visible) do visible - robj.visible = visible - end - - # remove tracking from GPUArrays - for uniform in values(uniforms) - if uniform isa GPUArray - foreach(off, uniform.requires_update.inputs) - empty!(uniform.requires_update.inputs) - end - end - for buffer in vertexarray.buffers - if buffer isa GPUArray - foreach(off, buffer.requires_update.inputs) - empty!(buffer.requires_update.inputs) - end - end - foreach(off, vertexarray.requires_update.inputs) - empty!(vertexarray.requires_update.inputs) + push!(observables, visible) + on(visible) do visible + robj.visible = visible + return end - return robj end end @@ -474,8 +416,7 @@ function RenderObject( vertexarray, pre, post, - visible, - track_updates + visible ) # automatically integrate object ID, will be discarded if shader doesn't use it @@ -502,7 +443,6 @@ function clean_up_observables(x::T) where T foreach(off, x.observers) empty!(x.observers) end - Observables.clear(x.requires_update) end # OpenGL has the annoying habit of reusing id's when creating a new context diff --git a/GLMakie/src/GLAbstraction/GLUniforms.jl b/GLMakie/src/GLAbstraction/GLUniforms.jl index a88ad9a9f97..a51af4024bc 100644 --- a/GLMakie/src/GLAbstraction/GLUniforms.jl +++ b/GLMakie/src/GLAbstraction/GLUniforms.jl @@ -241,6 +241,7 @@ gl_convert(x::Mat{N, M, T}) where {N, M, T} = map(gl_promote(T), x) gl_convert(a::AbstractVector{<: AbstractFace}) = indexbuffer(s) gl_convert(t::Type{T}, a::T; kw_args...) where T <: NATIVE_TYPES = a gl_convert(::Type{<: GPUArray}, a::StaticVector) = gl_convert(a) +gl_convert(x::Vector) = x function gl_convert(T::Type{<: GPUArray}, a::AbstractArray{X, N}; kw_args...) where {X, N} T(convert(AbstractArray{gl_promote(X), N}, a); kw_args...) @@ -261,16 +262,4 @@ function gl_convert(::Type{T}, a::Observable{<: AbstractArray{X, N}}; kw_args... T(s; kw_args...) end -lift_convert(a::AbstractArray, T, N) = lift(x -> convert(Array{T, N}, x), a) -function lift_convert(a::ShaderAbstractions.Sampler, T, N) - ShaderAbstractions.Sampler( - lift(x -> convert(Array{T, N}, x.data), a), - minfilter = a[].minfilter, magfilter = a[].magfilter, - x_repeat = a[].repeat[1], - y_repeat = a[].repeat[min(2, N)], - z_repeat = a[].repeat[min(3, N)], - anisotropic = a[].anisotropic, swizzle_mask = a[].swizzle_mask - ) -end - gl_convert(f::Function, a) = f(a) diff --git a/GLMakie/src/GLMakie.jl b/GLMakie/src/GLMakie.jl index 91a2b127204..5af488594da 100644 --- a/GLMakie/src/GLMakie.jl +++ b/GLMakie/src/GLMakie.jl @@ -43,7 +43,16 @@ export Sampler, Buffer const GL_ASSET_DIR = RelocatableFolders.@path joinpath(@__DIR__, "..", "assets") const SHADER_DIR = RelocatableFolders.@path joinpath(GL_ASSET_DIR, "shader") -loadshader(name) = joinpath(SHADER_DIR, name) +const LOADED_SHADERS = Dict{String, String}() + +function loadshader(name) + # Turns out, joinpath is so slow, that it actually makes sense + # To memoize it :-O + # when creating 1000 plots with the PlotSpec API, timing drop from 1.5s to 1s just from this change: + return get!(LOADED_SHADERS, name) do + return joinpath(SHADER_DIR, name) + end +end gl_texture_atlas() = Makie.get_texture_atlas(2048, 64) @@ -54,8 +63,6 @@ function __init__() activate!() end -Base.@deprecate set_window_config!(; screen_config...) GLMakie.activate!(; screen_config...) - include("precompiles.jl") end diff --git a/GLMakie/src/display.jl b/GLMakie/src/display.jl index c8d0aef1f72..5135fa1f1b5 100644 --- a/GLMakie/src/display.jl +++ b/GLMakie/src/display.jl @@ -10,4 +10,4 @@ function Base.display(screen::Screen, scene::Scene; connect=true) return screen end -Makie.backend_showable(::Type{Screen}, ::Union{MIME"image/jpeg", MIME"image/png"}) = true +Makie.backend_showable(::Type{Screen}, ::Union{MIME"image/jpeg", MIME"image/png", Makie.WEB_MIMES...}) = true diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 5e048f45c48..9fa55f52b8e 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -2,6 +2,107 @@ using Makie: transform_func_obs, apply_transform using Makie: attribute_per_char, FastPixel, el32convert, Pixel using Makie: convert_arguments +function handle_lights(attr::Dict, screen::Screen, lights::Vector{Makie.AbstractLight}) + @inline function push_inplace!(trg, idx, src) + for i in eachindex(src) + trg[idx + i] = src[i] + end + return idx + length(src) + end + + MAX_LIGHTS = screen.config.max_lights + MAX_PARAMS = screen.config.max_light_parameters + + # Every light has a type and a color. Therefore we have these as independent + # uniforms with a max length of MAX_LIGHTS. + # Other parameters like position, direction, etc differe between light types. + # To avoid wasting a bunch of memory we squash all of them into one vector of + # size MAX_PARAMS. + attr[:N_lights] = Observable(0) + attr[:light_types] = Observable(sizehint!(Int32[], MAX_LIGHTS)) + attr[:light_colors] = Observable(sizehint!(RGBf[], MAX_LIGHTS)) + attr[:light_parameters] = Observable(sizehint!(Float32[], MAX_PARAMS)) + + on(screen.render_tick, priority = typemin(Int)) do _ + # derive number of lights from available lights. Both MAX_LIGHTS and + # MAX_PARAMS are considered for this. + n_lights = 0 + n_params = 0 + for light in lights + delta = 0 + if light isa PointLight + delta = 5 # 3 position + 2 attenuation + elseif light isa DirectionalLight + delta = 3 # 3 direction + elseif light isa SpotLight + delta = 8 # 3 position + 3 direction + 2 angles + elseif light isa RectLight + delta = 12 # 3 position + 2x 3 rect basis vectors + 3 direction + end + if n_params + delta > MAX_PARAMS || n_lights == MAX_LIGHTS + if n_params > MAX_PARAMS + @warn "Exceeded the maximum number of light parameters ($n_params > $MAX_PARAMS). Skipping lights beyond number $n_lights." + else + @warn "Exceeded the maximum number of lights ($n_lights > $MAX_LIGHTS). Skipping lights beyond number $n_lights." + end + break + end + n_params += delta + n_lights += 1 + end + + # Update number of lights + attr[:N_lights][] = n_lights + + # Update light types + trg = attr[:light_types][] + resize!(trg, n_lights) + map!(i -> Makie.light_type(lights[i]), trg, 1:n_lights) + notify(attr[:light_types]) + + # Update light colors + trg = attr[:light_colors][] + resize!(trg, n_lights) + map!(i -> Makie.light_color(lights[i]), trg, 1:n_lights) + notify(attr[:light_colors]) + + # Update other light parameters + # This precalculates world space pos/dir -> view/cam space pos/dir + parameters = attr[:light_parameters][] + resize!(parameters, n_params) + idx = 0 + for i in 1:n_lights + light = lights[i] + if light isa PointLight + idx = push_inplace!(parameters, idx, light.position[]) + idx = push_inplace!(parameters, idx, light.attenuation[]) + elseif light isa DirectionalLight + if light.camera_relative + T = inv(attr[:view][][Vec(1,2,3), Vec(1,2,3)]) + dir = normalize(T * light.direction[]) + else + dir = normalize(light.direction[]) + end + idx = push_inplace!(parameters, idx, dir) + elseif light isa SpotLight + idx = push_inplace!(parameters, idx, light.position[]) + idx = push_inplace!(parameters, idx, normalize(light.direction[])) + idx = push_inplace!(parameters, idx, cos.(light.angles[])) + elseif light isa RectLight + idx = push_inplace!(parameters, idx, light.position[]) + idx = push_inplace!(parameters, idx, light.u1[]) + idx = push_inplace!(parameters, idx, light.u2[]) + idx = push_inplace!(parameters, idx, normalize(light.direction[])) + end + end + notify(attr[:light_parameters]) + + return Consume(false) + end + + return attr +end + Makie.el32convert(x::GLAbstraction.Texture) = x gpuvec(x) = GPUVector(GLBuffer(x)) @@ -37,31 +138,51 @@ function to_glvisualize_key(k) end function connect_camera!(plot, gl_attributes, cam, space = gl_attributes[:space]) - for key in (:pixel_space, :resolution, :eyeposition) + for key in (:pixel_space, :eyeposition) # Overwrite these, user defined attributes shouldn't use those! gl_attributes[key] = lift(identity, plot, getfield(cam, key)) end get!(gl_attributes, :view) do - return lift(plot, cam.view, space) do view, space - return is_data_space(space) ? view : Mat4f(I) + # get!(cam.calculated_values, Symbol("view_$(space[])")) do + return lift(plot, cam.view, space) do view, space + return is_data_space(space) ? view : Mat4f(I) + end + # end + end + + # for lighting + get!(gl_attributes, :world_normalmatrix) do + return lift(plot, gl_attributes[:model]) do m + i = Vec(1, 2, 3) + return transpose(inv(m[i, i])) end end - get!(gl_attributes, :normalmatrix) do + + # for SSAO + get!(gl_attributes, :view_normalmatrix) do return lift(plot, gl_attributes[:view], gl_attributes[:model]) do v, m i = Vec(1, 2, 3) return transpose(inv(v[i, i] * m[i, i])) end end - get!(gl_attributes, :projection) do - return lift(cam.projection, cam.pixel_space, space) do _, _, space - return Makie.space_to_clip(cam, space, false) - end + # return get!(cam.calculated_values, Symbol("projection_$(space[])")) do + return lift(plot, cam.projection, cam.pixel_space, space) do _, _, space + return Makie.space_to_clip(cam, space, false) + end + # end end - get!(gl_attributes, :projectionview) do - return lift(plot, cam.projectionview, cam.pixel_space, space) do _, _, space - Makie.space_to_clip(cam, space, true) + # get!(cam.calculated_values, Symbol("projectionview_$(space[])")) do + return lift(plot, cam.projectionview, cam.pixel_space, space) do _, _, space + Makie.space_to_clip(cam, space, true) + end + # end + end + # resolution in real hardware pixels, not scaled pixels/units + get!(gl_attributes, :resolution) do + get!(cam.calculated_values, :resolution) do + return lift(*, plot, gl_attributes[:px_per_unit], cam.resolution) end end @@ -70,9 +191,12 @@ function connect_camera!(plot, gl_attributes, cam, space = gl_attributes[:space] return nothing end -function handle_intensities!(attributes, plot) +function handle_intensities!(screen, attributes, plot) color = plot.calculated_colors if color[] isa Makie.ColorMapping + onany(plot, color[].color_scaled, color[].colorrange_scaled, color[].colormap, color[].nan_color) do args... + screen.requires_update = true + end attributes[:intensity] = color[].color_scaled interp = color[].color_mapping_type[] === Makie.continuous ? :linear : :nearest attributes[:color_map] = Texture(color[].colormap; minfilter=interp) @@ -97,53 +221,100 @@ function get_space(x) return haskey(x, :markerspace) ? x.markerspace : x.space end -function cached_robj!(robj_func, screen, scene, x::AbstractPlot) +const EXCLUDE_KEYS = Set([:transformation, :tickranges, :ticklabels, :raw, :SSAO, + :lightposition, :material, :axis_cycler, + :inspector_label, :inspector_hover, :inspector_clear, :inspectable, + :colorrange, :colormap, :colorscale, :highclip, :lowclip, :nan_color, + :calculated_colors, :space, :markerspace, :model]) + + +function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) # poll inside functions to make wait on compile less prominent pollevents(screen) - robj = get!(screen.cache, objectid(x)) do - filtered = filter(x.attributes) do (k, v) - !in(k, ( - :transformation, :tickranges, :ticklabels, :raw, :SSAO, - :lightposition, :material, - :inspector_label, :inspector_hover, :inspector_clear, :inspectable, - :colorrange, :colormap, :colorscale, :highclip, :lowclip, :nan_color, - :calculated_colors - )) - end + robj = get!(screen.cache, objectid(plot)) do + filtered = filter(plot.attributes) do (k, v) + return !in(k, EXCLUDE_KEYS) + end + track_updates = screen.config.render_on_demand + if track_updates + for arg in plot.args + on(plot, arg) do x + screen.requires_update = true + end + end + on(plot, plot.model) do x + screen.requires_update = true + end + on(plot, scene.camera.projectionview) do x + screen.requires_update = true + end + end gl_attributes = Dict{Symbol, Any}(map(filtered) do key_value key, value = key_value gl_key = to_glvisualize_key(key) - gl_value = lift_convert(key, value, x) + gl_value = lift_convert(key, value, plot, screen) gl_key => gl_value end) - - pointlight = Makie.get_point_light(scene) - if !isnothing(pointlight) - gl_attributes[:lightposition] = pointlight.position + gl_attributes[:model] = plot.model + if haskey(plot, :markerspace) + gl_attributes[:markerspace] = plot.markerspace end - - ambientlight = Makie.get_ambient_light(scene) - if !isnothing(ambientlight) - gl_attributes[:ambient] = ambientlight.color + gl_attributes[:space] = plot.space + gl_attributes[:px_per_unit] = screen.px_per_unit + + handle_intensities!(screen, gl_attributes, plot) + connect_camera!(plot, gl_attributes, scene.camera, get_space(plot)) + + # TODO: remove depwarn & conversion after some time + if haskey(gl_attributes, :shading) && to_value(gl_attributes[:shading]) isa Bool + @warn "`shading::Bool` is deprecated. Use `shading = NoShading` instead of false and `shading = FastShading` or `shading = MultiLightShading` instead of true." + gl_attributes[:shading] = ifelse(gl_attributes[:shading][], FastShading, NoShading) + elseif haskey(gl_attributes, :shading) && gl_attributes[:shading] isa Observable + gl_attributes[:shading] = gl_attributes[:shading][] end - gl_attributes[:track_updates] = screen.config.render_on_demand - handle_intensities!(gl_attributes, x) - connect_camera!(x, gl_attributes, scene.camera, get_space(x)) + shading = to_value(get(gl_attributes, :shading, NoShading)) - robj = robj_func(gl_attributes) + if shading == FastShading + dirlight = Makie.get_directional_light(scene) + if !isnothing(dirlight) + gl_attributes[:light_direction] = if dirlight.camera_relative + map(gl_attributes[:view], dirlight.direction) do view, dir + return normalize(inv(view[Vec(1,2,3), Vec(1,2,3)]) * dir) + end + else + map(normalize, dirlight.direction) + end - get!(gl_attributes, :ssao, Observable(false)) - screen.cache2plot[robj.id] = x + gl_attributes[:light_color] = dirlight.color + else + gl_attributes[:light_direction] = Observable(Vec3f(0)) + gl_attributes[:light_color] = Observable(RGBf(0,0,0)) + end - robj + ambientlight = Makie.get_ambient_light(scene) + if !isnothing(ambientlight) + gl_attributes[:ambient] = ambientlight.color + else + gl_attributes[:ambient] = Observable(RGBf(0,0,0)) + end + elseif shading == MultiLightShading + handle_lights(gl_attributes, screen, scene.lights) + end + robj = robj_func(gl_attributes) # <-- here + + get!(gl_attributes, :ssao, Observable(false)) + screen.cache2plot[robj.id] = plot + return robj end push!(screen, scene, robj) return robj end -function Base.insert!(screen::Screen, scene::Scene, x::Combined) +Base.insert!(::GLMakie.Screen, ::Scene, ::Makie.PlotList) = nothing + +function Base.insert!(screen::Screen, scene::Scene, @nospecialize(x::Plot)) ShaderAbstractions.switch_context!(screen.glscreen) # poll inside functions to make wait on compile less prominent pollevents(screen) @@ -158,12 +329,6 @@ function Base.insert!(screen::Screen, scene::Scene, x::Combined) end end -function remove_automatic!(attributes) - filter!(attributes) do (k, v) - to_value(v) != automatic - end -end - index1D(x::SubArray) = parentindices(x)[1] handle_view(array::AbstractVector, attributes) = array @@ -183,12 +348,13 @@ function handle_view(array::Observable{T}, attributes) where T <: SubArray return A end -function lift_convert(key, value, plot) - return lift_convert_inner(value, Key{key}(), Key{Makie.plotkey(plot)}(), plot) +function lift_convert(key, value, plot, screen) + return lift_convert_inner(value, Key{key}(), Key{Makie.plotkey(plot)}(), plot, screen) end -function lift_convert_inner(value, key, plot_key, plot) +function lift_convert_inner(value, key, plot_key, plot, screen) return lift(plot, value) do value + screen.requires_update = true return convert_attribute(value, key, plot_key) end end @@ -209,28 +375,28 @@ end pixel2world(scene, msize::AbstractVector) = pixel2world.(scene, msize) -function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Union{Scatter, MeshScatter})) - return cached_robj!(screen, scene, x) do gl_attributes +function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Union{Scatter, MeshScatter})) + return cached_robj!(screen, scene, plot) do gl_attributes # signals not supported for shading yet - gl_attributes[:shading] = to_value(get(gl_attributes, :shading, true)) - marker = lift_convert(:marker, pop!(gl_attributes, :marker), x) + marker = pop!(gl_attributes, :marker) - space = x.space - positions = handle_view(x[1], gl_attributes) - positions = apply_transform(transform_func_obs(x), positions, space) + space = plot.space + positions = handle_view(plot[1], gl_attributes) + positions = lift(apply_transform, plot, transform_func_obs(plot), positions, space) - if x isa Scatter - mspace = x.markerspace + if plot isa Scatter + mspace = plot.markerspace cam = scene.camera - gl_attributes[:preprojection] = map(space, mspace, cam.projectionview, cam.resolution) do space, mspace, _, _ + gl_attributes[:preprojection] = lift(plot, space, mspace, cam.projectionview, + cam.resolution) do space, mspace, _, _ return Makie.clip_to_space(cam, mspace) * Makie.space_to_clip(cam, space) end # fast pixel does its own setup if !(marker[] isa FastPixel) - gl_attributes[:billboard] = map(rot-> isa(rot, Billboard), x.rotations) + gl_attributes[:billboard] = lift(rot -> isa(rot, Billboard), plot, plot.rotations) atlas = gl_texture_atlas() isnothing(gl_attributes[:distancefield][]) && delete!(gl_attributes, :distancefield) - shape = lift(m-> Cint(Makie.marker_to_sdf_shape(m)), x, marker) + shape = lift(m -> Cint(Makie.marker_to_sdf_shape(m)), plot, marker) gl_attributes[:shape] = shape get!(gl_attributes, :distancefield) do if shape[] === Cint(DISTANCEFIELD) @@ -244,7 +410,8 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Union{Scatte get!(gl_attributes, :uv_offset_width) do return Makie.primitive_uv_offset_width(atlas, marker, font) end - scale, quad_offset = Makie.marker_attributes(atlas, marker, gl_attributes[:scale], font, gl_attributes[:quad_offset]) + scale, quad_offset = Makie.marker_attributes(atlas, marker, gl_attributes[:scale], font, + gl_attributes[:quad_offset], plot) gl_attributes[:scale] = scale gl_attributes[:quad_offset] = quad_offset end @@ -259,7 +426,7 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Union{Scatte end return draw_pixel_scatter(screen, positions, gl_attributes) else - if x isa MeshScatter + if plot isa MeshScatter if haskey(gl_attributes, :color) && to_value(gl_attributes[:color]) isa AbstractMatrix{<: Colorant} gl_attributes[:image] = gl_attributes[:color] end @@ -274,28 +441,31 @@ end _mean(xs) = sum(xs) / length(xs) # skip Statistics import -function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Lines)) - return cached_robj!(screen, scene, x) do gl_attributes +function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Lines)) + return cached_robj!(screen, scene, plot) do gl_attributes linestyle = pop!(gl_attributes, :linestyle) data = Dict{Symbol, Any}(gl_attributes) + positions = handle_view(plot[1], data) - positions = handle_view(x[1], data) - transform_func = transform_func_obs(x) - + transform_func = transform_func_obs(plot) ls = to_value(linestyle) - space = x.space + space = plot.space if isnothing(ls) data[:pattern] = ls data[:fast] = true - positions = apply_transform(transform_func, positions, space) + positions = lift(apply_transform, plot, transform_func, positions, space) else linewidth = gl_attributes[:thickness] - data[:pattern] = map((ls, lw) -> ls .* _mean(lw), linestyle, linewidth) + px_per_unit = data[:px_per_unit] + data[:pattern] = map(linestyle, linewidth, px_per_unit) do ls, lw, ppu + ppu * _mean(lw) .* ls + end data[:fast] = false - pvm = map(*, data[:projectionview], data[:model]) - positions = map(transform_func, positions, space, pvm, data[:resolution]) do f, ps, space, pvm, res + pvm = lift(*, plot, data[:projectionview], data[:model]) + positions = lift(plot, transform_func, positions, space, pvm, + data[:resolution]) do f, ps, space, pvm, res transformed = apply_transform(f, ps, space) output = Vector{Point3f}(undef, length(transformed)) scale = Vec3f(res[1], res[2], 1f0) @@ -310,48 +480,53 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Lines)) end end -function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::LineSegments)) - return cached_robj!(screen, scene, x) do gl_attributes +function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::LineSegments)) + return cached_robj!(screen, scene, plot) do gl_attributes linestyle = pop!(gl_attributes, :linestyle) data = Dict{Symbol, Any}(gl_attributes) + px_per_unit = data[:px_per_unit] ls = to_value(linestyle) if isnothing(ls) data[:pattern] = nothing data[:fast] = true else linewidth = gl_attributes[:thickness] - data[:pattern] = ls .* _mean(to_value(linewidth)) + data[:pattern] = lift(plot, linestyle, linewidth, px_per_unit) do ls, lw, ppu + ppu * _mean(lw) .* ls + end data[:fast] = false end - positions = handle_view(x.converted[1], data) - positions = apply_transform(transform_func_obs(x), positions, x.space) + positions = handle_view(plot[1], data) + + positions = lift(apply_transform, plot, transform_func_obs(plot), positions, plot.space) if haskey(data, :intensity) data[:color] = pop!(data, :intensity) end + return draw_linesegments(screen, positions, data) end end function draw_atomic(screen::Screen, scene::Scene, - x::Text{<:Tuple{<:Union{<:Makie.GlyphCollection, <:AbstractVector{<:Makie.GlyphCollection}}}}) - return cached_robj!(screen, scene, x) do gl_attributes - glyphcollection = x[1] + plot::Text{<:Tuple{<:Union{<:Makie.GlyphCollection, <:AbstractVector{<:Makie.GlyphCollection}}}}) + return cached_robj!(screen, scene, plot) do gl_attributes + glyphcollection = plot[1] - transfunc = Makie.transform_func_obs(x) + transfunc = Makie.transform_func_obs(plot) pos = gl_attributes[:position] - space = x.space - markerspace = x.markerspace + space = plot.space + markerspace = plot.markerspace offset = pop!(gl_attributes, :offset, Vec2f(0)) atlas = gl_texture_atlas() # calculate quad metrics - glyph_data = map(pos, glyphcollection, offset, transfunc, space) do pos, gc, offset, transfunc, space - Makie.text_quads(atlas, pos, to_value(gc), offset, transfunc, space) + glyph_data = lift(plot, pos, glyphcollection, offset, transfunc, space) do pos, gc, offset, transfunc, space + return Makie.text_quads(atlas, pos, to_value(gc), offset, transfunc, space) end # unpack values from the one signal: positions, char_offset, quad_offset, uv_offset_width, scale = map((1, 2, 3, 4, 5)) do i - lift(getindex, x, glyph_data, i) + lift(getindex, plot, glyph_data, i) end @@ -363,7 +538,7 @@ function draw_atomic(screen::Screen, scene::Scene, )) # space, end - gl_attributes[:color] = lift(x, glyphcollection) do gc + gl_attributes[:color] = lift(plot, glyphcollection) do gc if gc isa AbstractArray reduce(vcat, (Makie.collect_vector(g.colors, length(g.glyphs)) for g in gc), init = RGBAf[]) @@ -371,7 +546,7 @@ function draw_atomic(screen::Screen, scene::Scene, Makie.collect_vector(gc.colors, length(gc.glyphs)) end end - gl_attributes[:stroke_color] = lift(x, glyphcollection) do gc + gl_attributes[:stroke_color] = lift(plot, glyphcollection) do gc if gc isa AbstractArray reduce(vcat, (Makie.collect_vector(g.strokecolors, length(g.glyphs)) for g in gc), init = RGBAf[]) @@ -380,7 +555,7 @@ function draw_atomic(screen::Screen, scene::Scene, end end - gl_attributes[:rotation] = lift(x, glyphcollection) do gc + gl_attributes[:rotation] = lift(plot, glyphcollection) do gc if gc isa AbstractArray reduce(vcat, (Makie.collect_vector(g.rotations, length(g.glyphs)) for g in gc), init = Quaternionf[]) @@ -395,10 +570,10 @@ function draw_atomic(screen::Screen, scene::Scene, gl_attributes[:marker_offset] = char_offset gl_attributes[:uv_offset_width] = uv_offset_width gl_attributes[:distancefield] = get_texture!(atlas) - gl_attributes[:visible] = x.visible + gl_attributes[:visible] = plot.visible cam = scene.camera # gl_attributes[:preprojection] = Observable(Mat4f(I)) - gl_attributes[:preprojection] = map(space, markerspace, cam.projectionview, cam.resolution) do s, ms, pv, res + gl_attributes[:preprojection] = lift(plot, space, markerspace, cam.projectionview, cam.resolution) do s, ms, pv, res Makie.clip_to_space(cam, ms) * Makie.space_to_clip(cam, s) end @@ -412,12 +587,12 @@ xy_convert(x::AbstractArray{Float32}, n) = copy(x) xy_convert(x::AbstractArray, n) = el32convert(x) xy_convert(x, n) = Float32[LinRange(extrema(x)..., n + 1);] -function draw_atomic(screen::Screen, scene::Scene, heatmap::Heatmap) - return cached_robj!(screen, scene, heatmap) do gl_attributes - t = Makie.transform_func_obs(heatmap) - mat = heatmap[3] - space = heatmap.space # needs to happen before connect_camera! call - xypos = lift(t, heatmap[1], heatmap[2], space) do t, x, y, space +function draw_atomic(screen::Screen, scene::Scene, plot::Heatmap) + return cached_robj!(screen, scene, plot) do gl_attributes + t = Makie.transform_func_obs(plot) + mat = plot[3] + space = plot.space # needs to happen before connect_camera! call + xypos = lift(plot, t, plot[1], plot[2], space) do t, x, y, space x1d = xy_convert(x, size(mat[], 1)) y1d = xy_convert(y, size(mat[], 2)) # Only if transform doesn't do anything, we can stay linear in 1/2D @@ -435,12 +610,12 @@ function draw_atomic(screen::Screen, scene::Scene, heatmap::Heatmap) return (x1d, y1d) end end - xpos = map(first, xypos) - ypos = map(last, xypos) + xpos = lift(first, plot, xypos) + ypos = lift(last, plot, xypos) gl_attributes[:position_x] = Texture(xpos, minfilter = :nearest) gl_attributes[:position_y] = Texture(ypos, minfilter = :nearest) # number of planes used to render the heatmap - gl_attributes[:instances] = map(xpos, ypos) do x, y + gl_attributes[:instances] = lift(plot, xpos, ypos) do x, y (length(x)-1) * (length(y)-1) end interp = to_value(pop!(gl_attributes, :interpolate)) @@ -456,22 +631,21 @@ function draw_atomic(screen::Screen, scene::Scene, heatmap::Heatmap) end end -function draw_atomic(screen::Screen, scene::Scene, x::Image) - return cached_robj!(screen, scene, x) do gl_attributes - position = lift(x, x[1], x[2]) do x, y - r = to_range(x, y) - x, y = minimum(r[1]), minimum(r[2]) - xmax, ymax = maximum(r[1]), maximum(r[2]) - rect = Rect2f(x, y, xmax - x, ymax - y) +function draw_atomic(screen::Screen, scene::Scene, plot::Image) + return cached_robj!(screen, scene, plot) do gl_attributes + position = lift(plot, plot[1], plot[2]) do x, y + xmin, xmax = extrema(x) + ymin, ymax = extrema(y) + rect = Rect2f(xmin, ymin, xmax - xmin, ymax - ymin) return decompose(Point2f, rect) end - gl_attributes[:vertices] = apply_transform(transform_func_obs(x), position, x.space) + gl_attributes[:vertices] = lift(apply_transform, plot, transform_func_obs(plot), position, plot.space) rect = Rect2f(0, 0, 1, 1) gl_attributes[:faces] = decompose(GLTriangleFace, rect) gl_attributes[:texturecoordinates] = map(decompose_uv(rect)) do uv return 1.0f0 .- Vec2f(uv[2], uv[1]) end - gl_attributes[:shading] = false + get!(gl_attributes, :shading, NoShading) _interp = to_value(pop!(gl_attributes, :interpolate, true)) interp = _interp ? :linear : :nearest if haskey(gl_attributes, :intensity) @@ -483,10 +657,10 @@ function draw_atomic(screen::Screen, scene::Scene, x::Image) end end -function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, space=:data) +function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space=:data) # signals not supported for shading yet - shading = to_value(pop!(gl_attributes, :shading)) - gl_attributes[:shading] = shading + shading = to_value(gl_attributes[:shading])::Makie.MakieCore.ShadingAlgorithm + matcap_active = !isnothing(to_value(get(gl_attributes, :matcap, nothing))) color = pop!(gl_attributes, :color) interp = to_value(pop!(gl_attributes, :interpolate, true)) interp = interp ? :linear : :nearest @@ -495,18 +669,18 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, space=:data) delete!(gl_attributes, :color_map) delete!(gl_attributes, :color_norm) elseif to_value(color) isa Makie.AbstractPattern - img = lift(x -> el32convert(Makie.to_image(x)), color) + img = lift(x -> el32convert(Makie.to_image(x)), plot, color) gl_attributes[:image] = ShaderAbstractions.Sampler(img, x_repeat=:repeat, minfilter=:nearest) get!(gl_attributes, :fetch_pixel, true) elseif to_value(color) isa AbstractMatrix{<:Colorant} - gl_attributes[:image] = Texture(const_lift(el32convert, color), minfilter = interp) + gl_attributes[:image] = Texture(lift(el32convert, plot, color), minfilter = interp) delete!(gl_attributes, :color_map) delete!(gl_attributes, :color_norm) elseif to_value(color) isa AbstractMatrix{<: Number} - gl_attributes[:image] = Texture(const_lift(el32convert, color), minfilter = interp) + gl_attributes[:image] = Texture(lift(el32convert, plot, color), minfilter = interp) gl_attributes[:color] = nothing elseif to_value(color) isa AbstractVector{<: Union{Number, Colorant}} - gl_attributes[:vertex_color] = lift(el32convert, color) + gl_attributes[:vertex_color] = lift(el32convert, plot, color) else # error("Unsupported color type: $(typeof(to_value(color)))") end @@ -528,30 +702,32 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, space=:data) if hasproperty(to_value(mesh), :uv) gl_attributes[:texturecoordinates] = lift(decompose_uv, mesh) end - if hasproperty(to_value(mesh), :normals) && shading + if hasproperty(to_value(mesh), :normals) && (shading !== NoShading || matcap_active) gl_attributes[:normals] = lift(decompose_normals, mesh) end return draw_mesh(screen, gl_attributes) end function draw_atomic(screen::Screen, scene::Scene, meshplot::Mesh) - return cached_robj!(screen, scene, meshplot) do gl_attributes + x = cached_robj!(screen, scene, meshplot) do gl_attributes t = transform_func_obs(meshplot) space = meshplot.space # needs to happen before connect_camera! call - return mesh_inner(screen, meshplot[1], t, gl_attributes, space) + x = mesh_inner(screen, meshplot[1], t, gl_attributes, meshplot, space) + return x end + + return x end -function draw_atomic(screen::Screen, scene::Scene, x::Surface) - robj = cached_robj!(screen, scene, x) do gl_attributes +function draw_atomic(screen::Screen, scene::Scene, plot::Surface) + robj = cached_robj!(screen, scene, plot) do gl_attributes color = pop!(gl_attributes, :color) img = nothing - # signals not supported for shading yet # We automatically insert x[3] into the color channel, so if it's equal we don't need to do anything if haskey(gl_attributes, :intensity) img = pop!(gl_attributes, :intensity) elseif to_value(color) isa Makie.AbstractPattern - pattern_img = lift(x -> el32convert(Makie.to_image(x)), color) + pattern_img = lift(x -> el32convert(Makie.to_image(x)), plot, color) img = ShaderAbstractions.Sampler(pattern_img, x_repeat=:repeat, minfilter=:nearest) haskey(gl_attributes, :fetch_pixel) || (gl_attributes[:fetch_pixel] = true) gl_attributes[:color_map] = nothing @@ -564,18 +740,17 @@ function draw_atomic(screen::Screen, scene::Scene, x::Surface) gl_attributes[:color_norm] = nothing end - space = x.space + space = plot.space gl_attributes[:image] = img - gl_attributes[:shading] = to_value(get(gl_attributes, :shading, true)) - @assert to_value(x[3]) isa AbstractMatrix - types = map(v -> typeof(to_value(v)), x[1:2]) + @assert to_value(plot[3]) isa AbstractMatrix + types = map(v -> typeof(to_value(v)), plot[1:2]) if all(T -> T <: Union{AbstractMatrix, AbstractVector}, types) - t = Makie.transform_func_obs(x) - mat = x[3] - xypos = map(t, x[1], x[2], space) do t, x, y, space + t = Makie.transform_func_obs(plot) + mat = plot[3] + xypos = lift(plot, t, plot[1], plot[2], space) do t, x, y, space # Only if transform doesn't do anything, we can stay linear in 1/2D if Makie.is_identity_transform(t) return (x, y) @@ -590,18 +765,18 @@ function draw_atomic(screen::Screen, scene::Scene, x::Surface) return (first.(matrix), last.(matrix)) end end - xpos = map(first, xypos) - ypos = map(last, xypos) + xpos = lift(first, plot, xypos) + ypos = lift(last, plot, xypos) args = map((xpos, ypos, mat)) do arg - Texture(map(x-> convert(Array, el32convert(x)), arg); minfilter=:linear) + Texture(lift(x-> convert(Array, el32convert(x)), plot, arg); minfilter=:linear) end if isnothing(img) gl_attributes[:image] = args[3] end return draw_surface(screen, args, gl_attributes) else - gl_attributes[:ranges] = to_range.(to_value.(x[1:2])) - z_data = Texture(el32convert(x[3]); minfilter=:linear) + gl_attributes[:ranges] = to_range.(to_value.(plot[1:2])) + z_data = Texture(lift(el32convert, plot, plot[3]); minfilter=:linear) if isnothing(img) gl_attributes[:image] = z_data end @@ -611,11 +786,11 @@ function draw_atomic(screen::Screen, scene::Scene, x::Surface) return robj end -function draw_atomic(screen::Screen, scene::Scene, vol::Volume) - robj = cached_robj!(screen, scene, vol) do gl_attributes - model = vol[:model] - x, y, z = vol[1], vol[2], vol[3] - gl_attributes[:model] = lift(model, x, y, z) do m, xyz... +function draw_atomic(screen::Screen, scene::Scene, plot::Volume) + return cached_robj!(screen, scene, plot) do gl_attributes + model = plot.model + x, y, z = plot[1], plot[2], plot[3] + gl_attributes[:model] = lift(plot, model, x, y, z) do m, xyz... mi = minimum.(xyz) maxi = maximum.(xyz) w = maxi .- mi @@ -631,7 +806,7 @@ function draw_atomic(screen::Screen, scene::Scene, vol::Volume) intensity = pop!(gl_attributes, :intensity) return draw_volume(screen, intensity, gl_attributes) else - return draw_volume(screen, vol[4], gl_attributes) + return draw_volume(screen, plot[4], gl_attributes) end end end diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index 88bfb76b39a..52f631095ce 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -37,50 +37,50 @@ function Makie.disconnect!(window::GLFW.Window, ::typeof(window_open)) GLFW.SetWindowCloseCallback(window, nothing) end -function window_position(window::GLFW.Window) - xy = GLFW.GetWindowPos(window) - (xy.x, xy.y) -end +function Makie.window_area(scene::Scene, screen::Screen) + disconnect!(screen, window_area) -struct WindowAreaUpdater - window::GLFW.Window - dpi::Observable{Float64} - area::Observable{GeometryBasics.HyperRectangle{2, Int64}} -end + # TODO: Figure out which monitor the window is on and react to DPI changes + monitor = GLFW.GetPrimaryMonitor() + props = MonitorProperties(monitor) + scene.events.window_dpi[] = minimum(props.dpi) + + function windowsizecb(window, width::Cint, height::Cint) + area = scene.events.window_area + sf = screen.scalefactor[] -function (x::WindowAreaUpdater)(::Nothing) - ShaderAbstractions.switch_context!(x.window) - rect = x.area[] - # TODO put back window position, but right now it makes more trouble than it helps# - # x, y = GLFW.GetWindowPos(window) - # if minimum(rect) != Vec(x, y) - # event[] = Recti(x, y, framebuffer_size(window)) - # end - w, h = GLFW.GetFramebufferSize(x.window) - if Vec(w, h) != widths(rect) - monitor = GLFW.GetPrimaryMonitor() - props = MonitorProperties(monitor) - # dpi of a monitor should be the same in x y direction. - # if not, minimum seems to be a fair default - x.dpi[] = minimum(props.dpi) - x.area[] = Recti(minimum(rect), w, h) + ShaderAbstractions.switch_context!(window) + winscale = sf / (@static Sys.isapple() ? scale_factor(window) : 1) + winw, winh = round.(Int, (width, height) ./ winscale) + if Vec(winw, winh) != widths(area[]) + area[] = Recti(minimum(area[]), winw, winh) + end + return end - return -end + # TODO put back window position, but right now it makes more trouble than it helps + #function windowposcb(window, x::Cint, y::Cint) + # area = scene.events.window_area + # ShaderAbstractions.switch_context!(window) + # winscale = screen.scalefactor[] / (@static Sys.isapple() ? scale_factor(window) : 1) + # xs, ys = round.(Int, (x, y) ./ winscale) + # if Vec(xs, ys) != minimum(area[]) + # area[] = Recti(xs, ys, widths(area[])) + # end + # return + #end -function Makie.window_area(scene::Scene, screen::Screen) - disconnect!(screen, window_area) - - updater = WindowAreaUpdater( - to_native(screen), scene.events.window_dpi, scene.events.window_area - ) - on(updater, screen.render_tick) + window = to_native(screen) + GLFW.SetWindowSizeCallback(window, (win, w, h) -> windowsizecb(win, w, h)) + #GLFW.SetWindowPosCallback(window, (win, x, y) -> windowposcb(win, x, y)) + windowsizecb(window, Cint.(window_size(window))...) return end function Makie.disconnect!(screen::Screen, ::typeof(window_area)) - filter!(p -> !isa(p[2], WindowAreaUpdater), screen.render_tick.listeners) + window = to_native(screen) + #GLFW.SetWindowPosCallback(window, nothing) + GLFW.SetWindowSizeCallback(window, nothing) return end function Makie.disconnect!(::GLFW.Window, ::typeof(window_area)) @@ -168,49 +168,33 @@ function Makie.disconnect!(window::GLFW.Window, ::typeof(unicode_input)) GLFW.SetCharCallback(window, nothing) end -# TODO memoise? Or to bug ridden for the small performance gain? -function retina_scaling_factor(w, fb) - (w[1] == 0 || w[2] == 0) && return (1.0, 1.0) - fb ./ w -end - -# TODO both of these methods are slow! -# ~90µs, ~80µs -# This is too slow for events that may happen 100x per frame -function framebuffer_size(window::GLFW.Window) - wh = GLFW.GetFramebufferSize(window) - (wh.width, wh.height) -end -function window_size(window::GLFW.Window) - wh = GLFW.GetWindowSize(window) - (wh.width, wh.height) -end -function retina_scaling_factor(window::GLFW.Window) - w, fb = window_size(window), framebuffer_size(window) - retina_scaling_factor(w, fb) -end - -function correct_mouse(window::GLFW.Window, w, h) - ws, fb = window_size(window), framebuffer_size(window) - s = retina_scaling_factor(ws, fb) - (w * s[1], fb[2] - (h * s[2])) +function correct_mouse(screen::Screen, w, h) + nw = to_native(screen) + sf = screen.scalefactor[] / (@static Sys.isapple() ? scale_factor(nw) : 1) + _, winh = window_size(nw) + @static if Sys.isapple() + return w, (winh / sf) - h + else + return w / sf, (winh - h) / sf + end end struct MousePositionUpdater - window::GLFW.Window + screen::Screen mouseposition::Observable{Tuple{Float64, Float64}} hasfocus::Observable{Bool} end function (p::MousePositionUpdater)(::Nothing) !p.hasfocus[] && return - x, y = GLFW.GetCursorPos(p.window) - pos = correct_mouse(p.window, x, y) + nw = to_native(p.screen) + x, y = GLFW.GetCursorPos(nw) + pos = correct_mouse(p.screen, x, y) if pos != p.mouseposition[] @print_error p.mouseposition[] = pos # notify!(e.mouseposition) end - return + return Consume(false) end """ @@ -222,9 +206,9 @@ which is not in scene coordinates, with the upper left window corner being 0 function Makie.mouse_position(scene::Scene, screen::Screen) disconnect!(screen, mouse_position) updater = MousePositionUpdater( - to_native(screen), scene.events.mouseposition, scene.events.hasfocus + screen, scene.events.mouseposition, scene.events.hasfocus ) - on(updater, screen.render_tick) + on(updater, scene, screen.render_tick, priority = typemax(Int)) return end function Makie.disconnect!(screen::Screen, ::typeof(mouse_position)) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 1fb7a003f02..b8ed7a35dfd 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -75,5 +75,3 @@ include("rendering.jl") include("events.jl") include("drawing_primitives.jl") include("display.jl") - -Base.@deprecate_binding GLVisualize GLMakie true "The module `GLVisualize` has been removed and integrated into GLMakie, so simply replace all usage of `GLVisualize` with `GLMakie`." diff --git a/GLMakie/src/glshaders/image_like.jl b/GLMakie/src/glshaders/image_like.jl index 97c6fa990e8..0fd7ccbaa05 100644 --- a/GLMakie/src/glshaders/image_like.jl +++ b/GLMakie/src/glshaders/image_like.jl @@ -34,6 +34,7 @@ A matrix of Intensities will result in a contourf kind of plot function draw_heatmap(screen, data::Dict) primitive = triangle_mesh(Rect2(0f0,0f0,1f0,1f0)) to_opengl_mesh!(data, primitive) + pop!(data, :shading, FastShading) @gen_defaults! data begin intensity = nothing => Texture color_map = nothing => Texture @@ -55,6 +56,8 @@ end function draw_volume(screen, main::VolumeTypes, data::Dict) geom = Rect3f(Vec3f(0), Vec3f(1)) to_opengl_mesh!(data, const_lift(GeometryBasics.triangle_mesh, geom)) + shading = pop!(data, :shading, FastShading) + pop!(data, :backlight, 0f0) # We overwrite this @gen_defaults! data begin volumedata = main => Texture model = Mat4f(I) @@ -67,12 +70,17 @@ function draw_volume(screen, main::VolumeTypes, data::Dict) absorption = 1f0 isovalue = 0.5f0 isorange = 0.01f0 + backlight = 1f0 enable_depth = true transparency = false shader = GLVisualizeShader( screen, - "fragment_output.frag", "util.vert", "volume.vert", "volume.frag", + "util.vert", "volume.vert", + "fragment_output.frag", "lighting.frag", "volume.frag", view = Dict( + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "depth_init" => vol_depth_init(to_value(enable_depth)), "depth_default" => vol_depth_default(to_value(enable_depth)), "depth_main" => vol_depth_main(to_value(enable_depth)), diff --git a/GLMakie/src/glshaders/lines.jl b/GLMakie/src/glshaders/lines.jl index 86a25d916cf..562460de1f6 100644 --- a/GLMakie/src/glshaders/lines.jl +++ b/GLMakie/src/glshaders/lines.jl @@ -145,13 +145,13 @@ function draw_linesegments(screen, positions::VectorTypes{T}, data::Dict) where gl_primitive = GL_LINES pattern_length = 1f0 end - if !isa(pattern, Texture) && pattern !== nothing - if !isa(pattern, Vector) - error("Pattern needs to be a Vector of floats") + if !isa(pattern, Texture) && to_value(pattern) !== nothing + if !isa(to_value(pattern), Vector) + error("Pattern needs to be a Vector of floats. Found: $(typeof(pattern))") end - tex = GLAbstraction.Texture(ticks(pattern, 100), x_repeat = :repeat) + tex = GLAbstraction.Texture(map(pt -> ticks(pt, 100), pattern), x_repeat = :repeat) data[:pattern] = tex - data[:pattern_length] = Float32((last(pattern) - first(pattern))) + data[:pattern_length] = map(pt -> Float32(last(pt) - first(pt)), pattern) end robj = assemble_shader(data) return robj diff --git a/GLMakie/src/glshaders/mesh.jl b/GLMakie/src/glshaders/mesh.jl index 678d8f7b25f..877ddf90dea 100644 --- a/GLMakie/src/glshaders/mesh.jl +++ b/GLMakie/src/glshaders/mesh.jl @@ -27,7 +27,9 @@ function to_opengl_mesh!(result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) to_buffer(:uv, :texturecoordinates) to_buffer(:uvw, :texturecoordinates) # Only emit normals, when we shadin' - if to_value(get(result, :shading, true)) || !isnothing(to_value(get(result, :matcap, nothing))) + shading = get(result, :shading, NoShading)::Makie.MakieCore.ShadingAlgorithm + matcap_active = !isnothing(to_value(get(result, :matcap, nothing))) + if matcap_active || shading != NoShading to_buffer(:normals, :normals) end to_buffer(:attribute_id, :attribute_id) @@ -35,11 +37,11 @@ function to_opengl_mesh!(result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) end function draw_mesh(screen, data::Dict) + shading = pop!(data, :shading, NoShading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin vertices = nothing => GLBuffer faces = nothing => indexbuffer normals = nothing => GLBuffer - shading = true backlight = 0f0 vertex_color = nothing => GLBuffer image = nothing => Texture @@ -53,13 +55,18 @@ function draw_mesh(screen, data::Dict) interpolate_in_fragment_shader = true shader = GLVisualizeShader( screen, - "util.vert", "mesh.vert", "mesh.frag", "fragment_output.frag", + "util.vert", "mesh.vert", + "fragment_output.frag", "mesh.frag", + "lighting.frag", view = Dict( - "light_calc" => light_calc(shading), + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "buffers" => output_buffers(screen, to_value(transparency)), "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) ) end + return assemble_shader(data) end diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index f44bec57305..90e3aab1575 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -57,9 +57,9 @@ function draw_mesh_particle(screen, p, data) scale = Vec3f(1) => TextureBuffer rotation = rot => TextureBuffer texturecoordinates = nothing - shading = true end + shading = pop!(data, :shading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin color_map = nothing => Texture color_norm = nothing @@ -71,16 +71,19 @@ function draw_mesh_particle(screen, p, data) fetch_pixel = false interpolate_in_fragment_shader = false uv_scale = Vec2f(1) + backlight = 0f0 instances = const_lift(length, position) - shading = true transparency = false shader = GLVisualizeShader( screen, - "util.vert", "particles.vert", "mesh.frag", "fragment_output.frag", + "util.vert", "particles.vert", + "fragment_output.frag", "lighting.frag", "mesh.frag", view = Dict( "position_calc" => position_calc(position, nothing, nothing, nothing, TextureBuffer), - "light_calc" => light_calc(shading), + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "buffers" => output_buffers(screen, to_value(transparency)), "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) diff --git a/GLMakie/src/glshaders/surface.jl b/GLMakie/src/glshaders/surface.jl index c4d5961fc1a..ef7810a96f8 100644 --- a/GLMakie/src/glshaders/surface.jl +++ b/GLMakie/src/glshaders/surface.jl @@ -6,23 +6,29 @@ end function normal_calc(x::Bool, invert_normals::Bool = false) i = invert_normals ? "-" : "" if x - return "$(i)getnormal(position, position_x, position_y, position_z, o_uv);" + return "$(i)getnormal(position, position_x, position_y, position_z, index2D);" else return "vec3(0, 0, $(i)1);" end end +# TODO this shouldn't be necessary function light_calc(x::Bool) - if x - """ - vec3 L = normalize(o_lightdir); - vec3 N = normalize(o_normal); - vec3 light1 = blinnphong(N, o_camdir, L, color.rgb); - vec3 light2 = blinnphong(N, o_camdir, -L, color.rgb); - color = vec4(ambient * color.rgb + light1 + backlight * light2, color.a); - """ + @error "shading::Bool is deprecated. Use `NoShading` instead of `false` and `FastShading` or `MultiLightShading` instead of true." + return light_calc(ifelse(x, FastShading, NoShading)) +end + +function light_calc(x::Makie.MakieCore.ShadingAlgorithm) + if x === NoShading + return "#define NO_SHADING" + elseif x === FastShading + return "#define FAST_SHADING" + elseif x === MultiLightShading + return "#define MULTI_LIGHT_SHADING" + # elseif x === :PBR # TODO? else - "" + @warn "Did not recognize shading value :$x. Defaulting to FastShading." + return "#define FAST_SHADING" end end @@ -32,7 +38,8 @@ function _position_calc( """ int index1D = index + offseti.x + offseti.y * dims.x + (index/(dims.x-1)); ivec2 index2D = ind2sub(dims, index1D); - vec2 index01 = vec2(index2D) / (vec2(dims)-1.0); + vec2 index01 = (vec2(index2D) + 0.5) / (vec2(dims)); + pos = vec3( texelFetch(position_x, index2D, 0).x, texelFetch(position_y, index2D, 0).x, @@ -48,7 +55,8 @@ function _position_calc( """ int index1D = index + offseti.x + offseti.y * dims.x + (index/(dims.x-1)); ivec2 index2D = ind2sub(dims, index1D); - vec2 index01 = vec2(index2D) / (vec2(dims)-1.0); + vec2 index01 = (vec2(index2D) + 0.5) / (vec2(dims)); + pos = vec3( texelFetch(position_x, index2D.x, 0).x, texelFetch(position_y, index2D.y, 0).x, @@ -76,10 +84,11 @@ function _position_calc( grid::Grid{2}, position_z::MatTypes{T}, target::Type{Texture} ) where T<:AbstractFloat """ - int index1D = index + offseti.x + offseti.y * dims.x + (index/(dims.x-1)); + int index1D = index + offseti.x + offseti.y * dims.x; // + (index/(dims.x-1)); ivec2 index2D = ind2sub(dims, index1D); - vec2 index01 = vec2(index2D) / (vec2(dims)-1.0); - float height = texture(position_z, index01).x; + vec2 index01 = (vec2(index2D) + 0.5) / (vec2(dims)); + + float height = texelFetch(position_z, index2D, 0).x; pos = vec3(grid_pos(position, index01), height); """ end @@ -110,6 +119,7 @@ end function draw_surface(screen, main, data::Dict) primitive = triangle_mesh(Rect2(0f0,0f0,1f0,1f0)) to_opengl_mesh!(data, primitive) + shading = pop!(data, :shading, FastShading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin scale = nothing position = nothing @@ -117,8 +127,7 @@ function draw_surface(screen, main, data::Dict) position_y = nothing => Texture position_z = nothing => Texture image = nothing => Texture - shading = true - normal = shading + normal = shading != NoShading invert_normals = false backlight = 0f0 end @@ -138,12 +147,14 @@ function draw_surface(screen, main, data::Dict) transparency = false shader = GLVisualizeShader( screen, - "fragment_output.frag", "util.vert", "surface.vert", - "mesh.frag", + "util.vert", "surface.vert", + "fragment_output.frag", "lighting.frag", "mesh.frag", view = Dict( "position_calc" => position_calc(position, position_x, position_y, position_z, Texture), "normal_calc" => normal_calc(normal, to_value(invert_normals)), - "light_calc" => light_calc(shading), + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "buffers" => output_buffers(screen, to_value(transparency)), "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) diff --git a/GLMakie/src/glshaders/visualize_interface.jl b/GLMakie/src/glshaders/visualize_interface.jl index 1633084e6b6..d9236e4f61c 100644 --- a/GLMakie/src/glshaders/visualize_interface.jl +++ b/GLMakie/src/glshaders/visualize_interface.jl @@ -176,7 +176,7 @@ function output_buffer_writes(screen::Screen, transparency = false) """ fragment_color = color; fragment_position = o_view_pos; - fragment_normal_occlusion.xyz = o_normal; + fragment_normal_occlusion.xyz = o_view_normal; """ else "fragment_color = color;" diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index df0836b308b..7901b34c074 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -129,7 +129,7 @@ function GLFramebuffer(fb_size::NTuple{2, Int}) # To allow adding postprocessors in various combinations we need to keep # track of the buffer ids that are already in use. We may also want to reuse # buffers so we give them names for easy fetching. - buffer_ids = Dict( + buffer_ids = Dict{Symbol,GLuint}( :color => GL_COLOR_ATTACHMENT0, :objectid => GL_COLOR_ATTACHMENT1, :HDR_color => GL_COLOR_ATTACHMENT2, @@ -137,31 +137,29 @@ function GLFramebuffer(fb_size::NTuple{2, Int}) :depth => GL_DEPTH_ATTACHMENT, :stencil => GL_STENCIL_ATTACHMENT, ) - buffers = Dict( - :color => color_buffer, + buffers = Dict{Symbol, Texture}( + :color => color_buffer, :objectid => objectid_buffer, :HDR_color => HDR_color_buffer, :OIT_weight => OIT_weight_buffer, - :depth => depth_buffer, - :stencil => depth_buffer + :depth => depth_buffer, + :stencil => depth_buffer ) return GLFramebuffer( fb_size_node, frambuffer_id, buffer_ids, buffers, [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1] - ) + )::GLFramebuffer end -function Base.resize!(fb::GLFramebuffer, window_size) - ws = Int.((window_size[1], window_size[2])) - if ws != size(fb) && all(x-> x > 0, window_size) - for (name, buffer) in fb.buffers - resize_nocopy!(buffer, ws) - end - fb.resolution[] = ws +function Base.resize!(fb::GLFramebuffer, w::Int, h::Int) + (w > 0 && h > 0 && (w, h) != size(fb)) || return + for (name, buffer) in fb.buffers + resize_nocopy!(buffer, (w, h)) end - nothing + fb.resolution[] = (w, h) + return nothing end @@ -217,10 +215,21 @@ function destroy!(nw::GLFW.Window) was_current && ShaderAbstractions.switch_context!() end -function windowsize(nw::GLFW.Window) +function window_size(nw::GLFW.Window) + was_destroyed(nw) && return (0, 0) + return Tuple(GLFW.GetWindowSize(nw)) +end +function window_position(nw::GLFW.Window) was_destroyed(nw) && return (0, 0) - size = GLFW.GetFramebufferSize(nw) - return (size.width, size.height) + return Tuple(GLFW.GetWindowPos(window)) +end +function framebuffer_size(nw::GLFW.Window) + was_destroyed(nw) && return (0, 0) + return Tuple(GLFW.GetFramebufferSize(nw)) +end +function scale_factor(nw::GLFW.Window) + was_destroyed(nw) && return 1f0 + return minimum(GLFW.GetWindowContentScale(nw)) end function Base.isopen(window::GLFW.Window) diff --git a/GLMakie/src/picking.jl b/GLMakie/src/picking.jl index d83c4d6f9d2..ab718ba7bcc 100644 --- a/GLMakie/src/picking.jl +++ b/GLMakie/src/picking.jl @@ -6,16 +6,17 @@ function pick_native(screen::Screen, rect::Rect2i) isopen(screen) || return Matrix{SelectionID{Int}}(undef, 0, 0) ShaderAbstractions.switch_context!(screen.glscreen) - window_size = size(screen) fb = screen.framebuffer buff = fb.buffers[:objectid] glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) glReadBuffer(GL_COLOR_ATTACHMENT1) rx, ry = minimum(rect) rw, rh = widths(rect) - w, h = window_size - sid = zeros(SelectionID{UInt32}, widths(rect)...) + w, h = size(screen.root_scene) + ppu = screen.px_per_unit[] if rx > 0 && ry > 0 && rx + rw <= w && ry + rh <= h + rx, ry, rw, rh = round.(Int, ppu .* (rx, ry, rw, rh)) + sid = zeros(SelectionID{UInt32}, rw, rh) glReadPixels(rx, ry, rw, rh, buff.format, buff.pixeltype, sid) return sid else @@ -26,15 +27,16 @@ end function pick_native(screen::Screen, xy::Vec{2, Float64}) isopen(screen) || return SelectionID{Int}(0, 0) ShaderAbstractions.switch_context!(screen.glscreen) - sid = Base.RefValue{SelectionID{UInt32}}() - window_size = size(screen) fb = screen.framebuffer buff = fb.buffers[:objectid] glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) glReadBuffer(GL_COLOR_ATTACHMENT1) x, y = floor.(Int, xy) - w, h = window_size + w, h = size(screen.root_scene) + ppu = screen.px_per_unit[] if x > 0 && y > 0 && x <= w && y <= h + x, y = round.(Int, ppu .* (x, y)) + sid = Base.RefValue{SelectionID{UInt32}}() glReadPixels(x, y, 1, 1, buff.format, buff.pixeltype, sid) return convert(SelectionID{Int}, sid[]) end @@ -65,7 +67,7 @@ end # Skips one set of allocations function Makie.pick_closest(scene::Scene, screen::Screen, xy, range) isopen(screen) || return (nothing, 0) - w, h = size(screen) + w, h = size(scene) ((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) || return (nothing, 0) x0, y0 = max.(1, floor.(Int, xy .- range)) @@ -95,7 +97,7 @@ end # Skips some allocations function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) isopen(screen) || return (nothing, 0) - w, h = size(screen) + w, h = size(scene) if !((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) return Tuple{AbstractPlot, Int}[] end diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 3c295d158a6..60c978e07cf 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -163,6 +163,7 @@ function ssao_postprocessor(framebuffer, shader_cache) glDrawBuffer(normal_occ_id) # occlusion buffer glViewport(0, 0, w, h) glEnable(GL_SCISSOR_TEST) + ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) for (screenid, scene) in screen.screens # Select the area of one leaf scene @@ -170,8 +171,8 @@ function ssao_postprocessor(framebuffer, shader_cache) # scenes. It should be a leaf scene to avoid repeatedly shading # the same region (though this is not guaranteed...) isempty(scene.children) || continue - a = pixelarea(scene)[] - glScissor(minimum(a)..., widths(a)...) + a = viewport(scene)[] + glScissor(ppu(minimum(a))..., ppu(widths(a))...) # update uniforms data1[:projection] = scene.camera.projection[] data1[:bias] = scene.ssao.bias[] @@ -184,8 +185,8 @@ function ssao_postprocessor(framebuffer, shader_cache) for (screenid, scene) in screen.screens # Select the area of one leaf scene isempty(scene.children) || continue - a = pixelarea(scene)[] - glScissor(minimum(a)..., widths(a)...) + a = viewport(scene)[] + glScissor(ppu(minimum(a))..., ppu(widths(a))...) # update uniforms data2[:blur_range] = scene.ssao.blur GLAbstraction.render(pass2) @@ -285,14 +286,11 @@ function to_screen_postprocessor(framebuffer, shader_cache, screen_fb_id = nothi pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) full_render = screen -> begin - fb = screen.framebuffer - w, h = size(fb) - # transfer everything to the screen default_id = isnothing(screen_fb_id) ? 0 : screen_fb_id[] # GLFW uses 0, Gtk uses a value that we have to probe at the beginning of rendering glBindFramebuffer(GL_FRAMEBUFFER, default_id) - glViewport(0, 0, w, h) + glViewport(0, 0, framebuffer_size(screen.glscreen)...) glClear(GL_COLOR_BUFFER_BIT) GLAbstraction.render(pass) # copy postprocess end diff --git a/GLMakie/src/precompiles.jl b/GLMakie/src/precompiles.jl index 6ddcc86e980..d2bd372aa14 100644 --- a/GLMakie/src/precompiles.jl +++ b/GLMakie/src/precompiles.jl @@ -10,14 +10,18 @@ macro compile(block) end end + + let @setup_workload begin x = rand(5) @compile_workload begin + GLMakie.activate!() screen = GLMakie.singleton_screen(false) close(screen) destroy!(screen) + base_path = normpath(joinpath(dirname(pathof(Makie)), "..", "precompile")) shared_precompile = joinpath(base_path, "shared-precompile.jl") include(shared_precompile) @@ -26,6 +30,22 @@ let catch end Makie.CURRENT_FIGURE[] = nothing + + screen = Screen(Scene()) + close(screen) + screen = empty_screen(false) + close(screen) + destroy!(screen) + + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}()) + screen = Screen(Scene(), config, nothing, MIME"image/png"(); visible=false, start_renderloop=false) + close(screen) + + + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol,Any}()) + screen = Screen(Scene(), config; visible=false, start_renderloop=false) + close(screen) + empty!(atlas_texture_cache) closeall() @assert isempty(SCREEN_REUSE_POOL) @@ -35,3 +55,16 @@ let end nothing end + +precompile(Screen, (Scene, ScreenConfig)) +precompile(GLFramebuffer, (NTuple{2,Int},)) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{Float32})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBAf})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBf})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBA{N0f8}})) +precompile(glTexImage, + (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{GLAbstraction.DepthStencil_24_8})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{Vec{2,GLuint}})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBA{Float16}})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{N0f8})) +precompile(setindex!, (GLMakie.GLAbstraction.Texture{Float16,2}, Matrix{Float32}, Rect2{Int32})) diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index 3f9b5073c24..dda559f5ec3 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -1,14 +1,14 @@ - -function setup!(screen) +function setup!(screen::Screen) glEnable(GL_SCISSOR_TEST) - if isopen(screen) - glScissor(0, 0, size(screen)...) + if isopen(screen) && !isnothing(screen.root_scene) + ppu = screen.px_per_unit[] + glScissor(0, 0, round.(Int, size(screen.root_scene) .* ppu)...) glClearColor(1, 1, 1, 1) glClear(GL_COLOR_BUFFER_BIT) for (id, scene) in screen.screens if scene.visible[] - a = pixelarea(scene)[] - rt = (minimum(a)..., widths(a)...) + a = viewport(scene)[] + rt = (round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) glViewport(rt...) if scene.clear[] c = scene.backgroundcolor[] @@ -43,11 +43,10 @@ function render_frame(screen::Screen; resize_buffers=true) # render order here may introduce artifacts because of that. fb = screen.framebuffer - if resize_buffers - wh = Int.(framebuffer_size(nw)) - resize!(fb, wh) + if resize_buffers && !isnothing(screen.root_scene) + ppu = screen.px_per_unit[] + resize!(fb, round.(Int, ppu .* size(screen.root_scene))...) end - w, h = size(fb) # prepare stencil (for sub-scenes) glBindFramebuffer(GL_FRAMEBUFFER, fb.id) @@ -119,8 +118,9 @@ function GLAbstraction.render(filter_elem_func, screen::Screen) found, scene = id2scene(screen, screenid) found || continue scene.visible[] || continue - a = pixelarea(scene)[] - glViewport(minimum(a)..., widths(a)...) + ppu = screen.px_per_unit[] + a = viewport(scene)[] + glViewport(round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) render(elem) end catch e diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 5e934f6b6d5..8133ed61ed1 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -8,8 +8,7 @@ function renderloop end """ ## Renderloop -* `renderloop = GLMakie.renderloop`: sets a function `renderloop(::GLMakie.Screen)` which starts a renderloop for the screen. - +* `renderloop = GLMakie.renderloop`: Sets a function `renderloop(::GLMakie.Screen)` which starts a renderloop for the screen. !!! warning The keyword arguments below are not effective if `renderloop` isn't set to `GLMakie.renderloop`, unless implemented in a custom renderloop function: @@ -18,6 +17,7 @@ function renderloop end * `vsync = false`: Whether to enable vsync for the window. * `render_on_demand = true`: If `true`, the scene will only be rendered if something has changed in it. * `framerate = 30.0`: Sets the currently rendered frames per second. +* `px_per_unit = automatic`: Sets the ratio between the number of rendered pixels and the `Makie` resolution. It defaults to the value of `scalefactor` but may be any positive real number. ## GLFW window attributes * `float = false`: Whether the window should float above other windows. @@ -28,13 +28,16 @@ function renderloop end * `debugging = false`: If `true`, starts the GLFW.Window/OpenGL context with debug output. * `monitor::Union{Nothing, GLFW.Monitor} = nothing`: Sets the monitor on which the window should be opened. If set to `nothing`, GLFW will decide which monitor to use. * `visible = true`: Whether or not the window should be visible when first created. +* `scalefactor = automatic`: Sets the window scaling factor, such as `2.0` on HiDPI/Retina displays. It is set automatically based on the display, but may be any positive real number. -## Postprocessor +## Rendering constants & Postprocessor * `oit = false`: Whether to enable order independent transparency for the window. * `fxaa = true`: Whether to enable fxaa (anti-aliasing) for the window. * `ssao = true`: Whether to enable screen space ambient occlusion, which simulates natural shadowing at inner edges and crevices. * `transparency_weight_scale = 1000f0`: Adjusts a factor in the rendering shaders for order independent transparency. This should be the same for all of them (within one rendering pipeline) otherwise depth "order" will be broken. +* `max_lights = 64`: The maximum number of lights with `shading = MultiLightShading` +* `max_light_parameters = 5 * N_lights`: The maximum number of light parameters that can be uploaded. These include everything other than the light color (i.e. position, direction, attenuation, angles) in terms of scalar floats. """ mutable struct ScreenConfig # Renderloop @@ -43,6 +46,7 @@ mutable struct ScreenConfig vsync::Bool render_on_demand::Bool framerate::Float64 + px_per_unit::Union{Nothing, Float32} # GLFW window attributes float::Bool @@ -53,12 +57,15 @@ mutable struct ScreenConfig debugging::Bool monitor::Union{Nothing, GLFW.Monitor} visible::Bool + scalefactor::Union{Nothing, Float32} - # Postprocessor + # Render Constants & Postprocessor oit::Bool fxaa::Bool ssao::Bool transparency_weight_scale::Float32 + max_lights::Int + max_light_parameters::Int function ScreenConfig( # Renderloop @@ -67,6 +74,7 @@ mutable struct ScreenConfig vsync::Bool, render_on_demand::Bool, framerate::Number, + px_per_unit::Union{Makie.Automatic, Number}, # GLFW window attributes float::Bool, focus_on_show::Bool, @@ -76,12 +84,15 @@ mutable struct ScreenConfig debugging::Bool, monitor::Union{Nothing, GLFW.Monitor}, visible::Bool, + scalefactor::Union{Makie.Automatic, Number}, # Preprocessor oit::Bool, fxaa::Bool, ssao::Bool, - transparency_weight_scale::Number) + transparency_weight_scale::Number, + max_lights::Int, + max_light_parameters::Int) return new( # Renderloop renderloop isa Makie.Automatic ? GLMakie.renderloop : renderloop, @@ -89,6 +100,7 @@ mutable struct ScreenConfig vsync, render_on_demand, framerate, + px_per_unit isa Makie.Automatic ? nothing : Float32(px_per_unit), # GLFW window attributes float, focus_on_show, @@ -98,11 +110,15 @@ mutable struct ScreenConfig debugging, monitor, visible, + scalefactor isa Makie.Automatic ? nothing : Float32(scalefactor), + # Preproccessor # Preprocessor oit, fxaa, ssao, - transparency_weight_scale) + transparency_weight_scale, + max_lights, + max_light_parameters) end end @@ -147,6 +163,7 @@ mutable struct Screen{GLWindow} <: MakieScreen config::Union{Nothing, ScreenConfig} stop_renderloop::Bool rendertask::Union{Task, Nothing} + px_per_unit::Observable{Float32} screen2scene::Dict{WeakRef, ScreenID} screens::Vector{ScreenArea} @@ -155,8 +172,9 @@ mutable struct Screen{GLWindow} <: MakieScreen cache::Dict{UInt64, RenderObject} cache2plot::Dict{UInt32, AbstractPlot} framecache::Matrix{RGB{N0f8}} - render_tick::Observable{Nothing} + render_tick::Observable{Nothing} # listeners must not Consume(true) window_open::Observable{Bool} + scalefactor::Observable{Float32} root_scene::Union{Scene, Nothing} reuse::Bool @@ -185,16 +203,18 @@ mutable struct Screen{GLWindow} <: MakieScreen screen = new{GLWindow}( glscreen, shader_cache, framebuffer, config, stop_renderloop, rendertask, - screen2scene, + Observable(0f0), screen2scene, screens, renderlist, postprocessors, cache, cache2plot, Matrix{RGB{N0f8}}(undef, s), Observable(nothing), - Observable(true), nothing, reuse, true, false + Observable(true), Observable(0f0), nothing, reuse, true, false ) push!(ALL_SCREENS, screen) # track all created screens return screen end end +Makie.isvisible(screen::Screen) = screen.config.visible + # for e.g. closeall, track all created screens # gets removed in destroy!(screen) const ALL_SCREENS = Set{Screen}() @@ -213,6 +233,8 @@ function empty_screen(debugging::Bool; reuse=true) (GLFW.STENCIL_BITS, 0), (GLFW.AUX_BUFFERS, 0), + + (GLFW.SCALE_TO_MONITOR, true), ] resolution = (10, 10) window = try @@ -261,7 +283,9 @@ function empty_screen(debugging::Bool; reuse=true) Dict{UInt32, AbstractPlot}(), reuse, ) - GLFW.SetWindowRefreshCallback(window, window -> refreshwindowcb(window, screen)) + GLFW.SetWindowRefreshCallback(window, refreshwindowcb(screen)) + GLFW.SetWindowContentScaleCallback(window, scalechangecb(screen)) + return screen end @@ -276,6 +300,7 @@ function reopen!(screen::Screen) end @assert isempty(screen.window_open.listeners) screen.window_open[] = true + on(scalechangeobs(screen), screen.scalefactor) @assert isopen(screen) return screen end @@ -307,8 +332,6 @@ function singleton_screen(debugging::Bool) return reopen!(screen) end -const GLFW_FOCUS_ON_SHOW = 0x0002000C - function Makie.apply_screen_config!(screen::Screen, config::ScreenConfig, scene::Scene, args...) apply_config!(screen, config) end @@ -317,7 +340,7 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B @debug("Applying screen config! to existing screen") glw = screen.glscreen ShaderAbstractions.switch_context!(glw) - GLFW.SetWindowAttrib(glw, GLFW_FOCUS_ON_SHOW, config.focus_on_show) + GLFW.SetWindowAttrib(glw, GLFW.FOCUS_ON_SHOW, config.focus_on_show) GLFW.SetWindowAttrib(glw, GLFW.DECORATED, config.decorated) GLFW.SetWindowAttrib(glw, GLFW.FLOATING, config.float) GLFW.SetWindowTitle(glw, config.title) @@ -325,7 +348,8 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B if !isnothing(config.monitor) GLFW.SetWindowMonitor(glw, config.monitor) end - + screen.scalefactor[] = !isnothing(config.scalefactor) ? config.scalefactor : scale_factor(glw) + screen.px_per_unit[] = !isnothing(config.px_per_unit) ? config.px_per_unit : screen.scalefactor[] function replace_processor!(postprocessor, idx) fb = screen.framebuffer shader_cache = screen.shader_cache @@ -340,6 +364,9 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B replace_processor!(config.ssao ? ssao_postprocessor : empty_postprocessor, 1) replace_processor!(config.oit ? OIT_postprocessor : empty_postprocessor, 2) replace_processor!(config.fxaa ? fxaa_postprocessor : empty_postprocessor, 3) + + # TODO: replace shader programs with lighting to update N_lights & N_light_parameters + # Set the config screen.config = config if start_renderloop @@ -347,7 +374,9 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B else stop_renderloop!(screen) end - + if !isnothing(screen.root_scene) + resize!(screen, size(screen.root_scene)...) + end set_screen_visibility!(screen, config.visible) return screen end @@ -358,12 +387,12 @@ function Screen(; screen_config... ) # Screen config is managed by the current active theme, so managed by Makie - config = Makie.merge_screen_config(ScreenConfig, screen_config) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) screen = screen_from_pool(config.debugging) + apply_config!(screen, config; start_renderloop=start_renderloop) if !isnothing(resolution) resize!(screen, resolution...) end - apply_config!(screen, config; start_renderloop=start_renderloop) return screen end @@ -384,7 +413,7 @@ function display_scene!(screen::Screen, scene::Scene) end function Screen(scene::Scene; start_renderloop=true, screen_config...) - config = Makie.merge_screen_config(ScreenConfig, screen_config) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) return Screen(scene, config; start_renderloop=start_renderloop) end @@ -417,8 +446,9 @@ end function pollevents(screen::Screen) ShaderAbstractions.switch_context!(screen.glscreen) - notify(screen.render_tick) GLFW.PollEvents() + notify(screen.render_tick) + return end Base.wait(x::Screen) = !isnothing(x.rendertask) && wait(x.rendertask) @@ -436,10 +466,10 @@ function Makie.insertplots!(screen::Screen, scene::Scene) push!(screen.screens, (id, scene)) screen.requires_update = true onany( - (_, _, _, _, _, _) -> screen.requires_update = true, + (args...) -> screen.requires_update = true, scene, scene.visible, scene.backgroundcolor, scene.clear, - scene.ssao.bias, scene.ssao.blur, scene.ssao.radius + scene.ssao.bias, scene.ssao.blur, scene.ssao.radius, scene.camera.projectionview, scene.camera.resolution ) return id end @@ -556,6 +586,8 @@ function Base.empty!(screen::Screen) empty!(screen.screen2scene) empty!(screen.screens) + Observables.clear(screen.px_per_unit) + Observables.clear(screen.scalefactor) Observables.clear(screen.render_tick) Observables.clear(screen.window_open) GLFW.PollEvents() @@ -569,7 +601,10 @@ function destroy!(screen::Screen) # otherwise, during rendertask clean up we may run into a destroyed window wait(screen) screen.rendertask = nothing - destroy!(screen.glscreen) + window = screen.glscreen + GLFW.SetWindowRefreshCallback(window, nothing) + GLFW.SetWindowContentScaleCallback(window, nothing) + destroy!(window) # Since those are sets, we can just delete them from there, even if they weren't in there (e.g. reuse=false) delete!(SCREEN_REUSE_POOL, screen) delete!(ALL_SCREENS, screen) @@ -622,24 +657,30 @@ function closeall() return end -function resize_native!(window::GLFW.Window, resolution...) - if isopen(window) - ShaderAbstractions.switch_context!(window) - oldsize = windowsize(window) - retina_scale = retina_scaling_factor(window) - w, h = resolution ./ retina_scale - if oldsize == (w, h) - return - end - GLFW.SetWindowSize(window, round(Int, w), round(Int, h)) +function Base.resize!(screen::Screen, w::Int, h::Int) + window = to_native(screen) + (w > 0 && h > 0 && isopen(window)) || return nothing + + # Resize the window which appears on the user desktop (if necessary). + # + # On OSX with a Retina display, the window size is given in logical dimensions and + # is automatically scaled by the OS. To support arbitrary scale factors, we must account + # for the native scale factor when calculating the effective scaling to apply. + # + # On Linux and Windows, scale from the logical size to the pixel size. + ShaderAbstractions.switch_context!(window) + winscale = screen.scalefactor[] / (@static Sys.isapple() ? scale_factor(window) : 1) + winw, winh = round.(Int, winscale .* (w, h)) + if window_size(window) != (winw, winh) + GLFW.SetWindowSize(window, winw, winh) end -end -function Base.resize!(screen::Screen, w, h) - nw = to_native(screen) - resize_native!(nw, w, h) - fb = screen.framebuffer - resize!(fb, (w, h)) + # Then resize the underlying rendering framebuffers as well, which can be scaled + # independently of the window scale factor. + fbscale = screen.px_per_unit[] + fbw, fbh = round.(Int, fbscale .* (w, h)) + resize!(screen.framebuffer, fbw, fbh) + return nothing end function fast_color_data!(dest::Array{RGB{N0f8}, 2}, source::Texture{T, 2}) where T @@ -686,7 +727,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma ctex = screen.framebuffer.buffers[:color] # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! - # GLFW.PollEvents() + pollevents(screen) # keep current buffer size to allows larger-than-window renders render_frame(screen, resize_buffers=false) # let it render if screen.config.visible @@ -818,12 +859,32 @@ function set_framerate!(screen::Screen, fps=30) screen.config.framerate = fps end -function refreshwindowcb(window, screen) +function refreshwindowcb(screen, window) screen.render_tick[] = nothing render_frame(screen) GLFW.SwapBuffers(window) return end +refreshwindowcb(screen) = window -> refreshwindowcb(screen, window) + +function scalechangecb(screen, window, xscale, yscale) + sf = min(xscale, yscale) + if isnothing(screen.config.px_per_unit) && screen.scalefactor[] == screen.px_per_unit[] + screen.px_per_unit[] = sf + end + screen.scalefactor[] = sf + return +end +scalechangecb(screen) = (window, xscale, yscale) -> scalechangecb(screen, window, xscale, yscale) + +function scalechangeobs(screen, _) + if !isnothing(screen.root_scene) + resize!(screen, size(screen.root_scene)...) + end + return nothing +end +scalechangeobs(screen) = scalefactor -> scalechangeobs(screen, scalefactor) + # TODO add render_tick event to scene events function vsynced_renderloop(screen) @@ -865,9 +926,7 @@ function requires_update(screen::Screen) screen.requires_update = false return true end - for (_, _, robj) in screen.renderlist - robj.requires_update && return true - end + return false end diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 5a386d5d0e8..4c08416ffda 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -81,7 +81,7 @@ end glFinish() end end - fig, ax, meshplot = meshscatter(RNG.rand(Point3f, 10^4) .* 20f0) + fig, ax, meshplot = meshscatter(RNG.rand(Point3f, 10^4) .* 20f0; color=:black) screen = display(GLMakie.Screen(;renderloop=(screen) -> nothing, start_renderloop=false), fig.scene) buff = RNG.rand(Point3f, 10^4) .* 20f0; update_loop(meshplot, buff, screen) @@ -97,9 +97,70 @@ end fig = Figure() left = LScene(fig[1, 1]) contour!(left, [sin(i+j) * sin(j+k) * sin(i+k) for i in 1:10, j in 1:10, k in 1:10], enable_depth = true) - mesh!(left, Sphere(Point3f(5), 6f0)) + mesh!(left, Sphere(Point3f(5), 6f0), color=:black) right = LScene(fig[1, 2]) volume!(right, [sin(2i) * sin(2j) * sin(2k) for i in 1:10, j in 1:10, k in 1:10], algorithm = :iso, enable_depth = true) - mesh!(right, Sphere(Point3f(5), 6f0)) + mesh!(right, Sphere(Point3f(5), 6.0f0); color=:black) fig end + +@reference_test "Complex Lighting - Ambient + SpotLights + PointLights" begin + angle2pos(phi) = Point3f(cosd(phi), sind(phi), 0) + lights = [ + AmbientLight(RGBf(0.1, 0.1, 0.1)), + SpotLight(RGBf(2,0,0), angle2pos(0), Vec3f(0, 0, -1), Vec2f(pi/5, pi/4)), + SpotLight(RGBf(0,2,0), angle2pos(120), Vec3f(0, 0, -1), Vec2f(pi/5, pi/4)), + SpotLight(RGBf(0,0,2), angle2pos(240), Vec3f(0, 0, -1), Vec2f(pi/5, pi/4)), + PointLight(RGBf(1,1,1), Point3f(-4, -4, -2.5), 10.0), + PointLight(RGBf(1,1,0), Point3f(-4, 4, -2.5), 10.0), + PointLight(RGBf(1,0,1), Point3f( 4, 4, -2.5), 10.0), + PointLight(RGBf(0,1,1), Point3f( 4, -4, -2.5), 10.0), + ] + + scene = Scene(size = (400, 400), camera = cam3d!, lights = lights) + mesh!( + scene, + Rect3f(Point3f(-10, -10, -2.99), Vec3f(20, 20, 0.02)), + color = :white, shading = MultiLightShading, specular = Vec3f(0) + ) + center!(scene) + update_cam!(scene, Vec3f(0, 0, 10), Vec3f(0, 0, 0), Vec3f(0, 1, 0)) + scene +end + +@reference_test "Complex Lighting - DirectionalLight + specular reflection" begin + angle2dir(phi) = Vec3f(cosd(phi), sind(phi), -2) + lights = [ + AmbientLight(RGBf(0.1, 0.1, 0.1)), + DirectionalLight(RGBf(1,0,0), angle2dir(0)), + DirectionalLight(RGBf(0,1,0), angle2dir(120)), + DirectionalLight(RGBf(0,0,1), angle2dir(240)), + ] + + scene = Scene(size = (400, 400), camera = cam3d!, center = false, lights = lights, backgroundcolor = :black) + mesh!( + scene, Sphere(Point3f(0), 1f0), color = :white, shading = MultiLightShading, + specular = Vec3f(1), shininess = 16f0 + ) + update_cam!(scene, Vec3f(0, 0, 3), Vec3f(0, 0, 0), Vec3f(0, 1, 0)) + scene +end + + +@reference_test "RectLight" begin + lights = Makie.AbstractLight[ + RectLight(RGBf(0.5, 0, 0), Point3f(-0.5, -1, 2), Vec3f(3, 0, 0), Vec3f(0, 3, 0)), + RectLight(RGBf(0, 0.5, 0), Rect2f(-1, 1, 1, 3)), + RectLight(RGBf(0, 0, 0.5), Point3f( 1, 0.5, 2), Vec3f(3, 0, 0), Vec3f(0, 3, 0)), + RectLight(RGBf(0.5, 0.5, 0.5), Point3f( 1, -1, 2), Vec3f(3, 0, 0), Vec3f(0, 3, 0), Vec3f(-0.3, 0.3, -1)), + ] + # Test transformations + translate!(lights[2], Vec3f(-1, 1, 2)) # translate to by default + scale!(lights[2], 3, 1) + + scene = Scene(lights = lights, camera = cam3d!, size = (400, 400)) + p = mesh!(scene, Rect3f(Point3f(-10, -10, 0.01), Vec3f(20, 20, 0.02)), color = :white) + update_cam!(scene, Vec3f(0, 0, 7), Vec3f(0, 0, 0), Vec3f(0, 1, 0)) + + scene +end \ No newline at end of file diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index b235938df7d..1ada23c2012 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -11,8 +11,7 @@ if !GLMakie.ModernGL.enable_opengl_debugging @warn("TESTING WITHOUT OPENGL DEBUGGING") end - -GLMakie.activate!(framerate=1.0) +GLMakie.activate!(framerate=1.0, scalefactor=1.0) @testset "mimes" begin Makie.inline!(true) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 28da77684b0..bb7b7b650cf 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -2,7 +2,7 @@ using GLMakie.Makie: getscreen function project_sp(scene, point) point_px = Makie.project(scene, point) - offset = Point2f(minimum(pixelarea(scene)[])) + offset = Point2f(minimum(viewport(scene)[])) return point_px .+ offset end @@ -211,8 +211,9 @@ end @testset "stresstest multi displays" begin GLMakie.closeall() + set_theme!() screens = map(1:10) do i - fig = Figure(resolution=(500, 500)) + fig = Figure(size=(500, 500)) rng = Random.MersenneTwister(0) ax, pl = image(fig[1, 1], 0..1, 0..1, rand(rng, 1000, 1000)) scatter!(ax, rand(rng, Point2f, 1000), color=:red) @@ -224,13 +225,13 @@ end heatmap(fig[2, 1], rand(rng, 100, 100)) surface(fig[2, 2], 0..1, 0..1, rand(rng, 1000, 1000) ./ 2) - display(GLMakie.Screen(visible=false), fig) + display(GLMakie.Screen(visible=false, scalefactor=1), fig) end images = map(Makie.colorbuffer, screens) @test all(x-> x ≈ first(images), images) - @test Base.summarysize(screens) / 10^6 > 280 + @test Base.summarysize(screens) / 10^6 > 60 foreach(close, screens) for screen in screens @@ -244,10 +245,12 @@ end @test isempty(screen.window_open.listeners) @test isempty(screen.render_tick.listeners) + @test isempty(screen.px_per_unit.listeners) + @test isempty(screen.scalefactor.listeners) @test screen.root_scene === nothing @test screen.rendertask === nothing - @test (Base.summarysize(screen) / 10^6) < 1.2 + @test (Base.summarysize(screen) / 10^6) < 1.22 end # All should go to pool after close @test all(x-> x in GLMakie.SCREEN_REUSE_POOL, screens) @@ -256,3 +259,137 @@ end # now every screen should be gone @test isempty(GLMakie.SCREEN_REUSE_POOL) end + +@testset "HiDPI displays" begin + import FileIO: @format_str, File, load + set_theme!() + GLMakie.closeall() + + W, H = 400, 400 + N = 51 + x = collect(range(0.0, 2π, length=N)) + y = sin.(x) + fig, ax, pl = scatter(x, y, figure = (; size = (W, H))); + hidedecorations!(ax) + + # On OSX, the native window size has an underlying scale factor that we need to account + # for when interpreting native window sizes with respect to the desired figure size + # and desired scaling factor. + function scaled(screen::GLMakie.Screen, dims::Tuple{Vararg{Int}}) + sf = screen.scalefactor[] / (Sys.isapple() ? GLMakie.scale_factor(screen.glscreen) : 1) + return round.(Int, dims .* sf) + end + + screen = display(GLMakie.Screen(visible = true, scalefactor = 2), fig) + @test screen.scalefactor[] === 2f0 + @test screen.px_per_unit[] === 2f0 # inherited from scale factor + winscale = screen.scalefactor[] / (@static Sys.isapple() ? GLMakie.scale_factor(screen.glscreen) : 1) + @test size(screen.framebuffer) == (2W, 2H) + @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) + + # check that picking works through the resized GL buffers + GLMakie.Makie.colorbuffer(screen) # force render + # - point pick + point_px = project_sp(ax.scene, Point2f(x[end÷2], y[end÷2])) + elem, idx = pick(ax.scene, point_px) + @test elem === pl + @test idx == length(x) ÷ 2 + # - area pick + bottom_px = project_sp(ax.scene, Point2f(π, -1)) + right_px = project_sp(ax.scene, Point2f(2π, 0)) + quadrant = Rect2i(round.(bottom_px)..., round.(right_px - bottom_px)...) + picks = pick(ax.scene, quadrant) + points = Set(Int(p[2]) for p in picks if p[1] isa Scatter) + @test points == Set(((N+1)÷2):N) + + # render at lower resolution + screen = display(GLMakie.Screen(visible = false, scalefactor = 2, px_per_unit = 1), fig) + @test screen.scalefactor[] === 2f0 + @test screen.px_per_unit[] === 1f0 + @test size(screen.framebuffer) == (W, H) + + # decrease the scale factor after-the-fact + screen.scalefactor[] = 1 + GLMakie.Makie.colorbuffer(screen) + @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) + + # save images of different resolutions + mktemp() do path, io + close(io) + file = File{format"PNG"}(path) + + # save at current size + @test screen.px_per_unit[] == 1 + save(file, fig; px_per_unit=1) + img = load(file) + @test size(img) == (W, H) + # save with a different resolution + save(file, fig, px_per_unit = 2) + img = load(file) + @test size(img) == (2W, 2H) + # writing to file should not effect the visible figure + @test_broken screen.px_per_unit[] == 1 + # Make sure switching back resizes the screen correctly! + save(file, fig; px_per_unit=1) + img = load(file) + @test size(img) == (W, H) + end + + # make sure there isn't a race between changing the scale factor and window_area updater + # see https://github.com/MakieOrg/Makie.jl/pull/2544#issuecomment-1416861800 + screen = display(GLMakie.Screen(visible = false, scalefactor = 2, framerate = 60), fig) + @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) + on(screen.scalefactor) do sf + sleep(0.5) + end + screen.scalefactor[] = 1 + @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) + + if Sys.islinux() + # Test that GLMakie is correctly getting the default scale factor from X11 in a + # HiDPI environment. + + checkcmd = `which xrdb` & `which xsettingsd` + checkcmd = pipeline(ignorestatus(checkcmd), stdout = devnull, stderr = devnull) + hasxrdb = success(run(checkcmd)) + + # Only continue if running within an Xvfb environment where the setting is + # empty by default. Overriding during a user's session could be problematic + # (i.e. if running interactively rather than in CI). + inxvfb = hasxrdb ? isempty(readchomp(`xrdb -query`)) : false + + if hasxrdb && inxvfb + # GLFW looks for Xft.dpi resource setting. Spawn a temporary xsettingsd daemon + # to be the X resource manager + xsettingsd = run(pipeline(`xsettingsd -c /dev/null`), wait = false) + try + # Then set the DPI to 192, i.e. 2 times the default of 96dpi + run(pipeline(`echo "Xft.dpi: 192"`, `xrdb -merge`)) + + # Print out the automatically-determined scale factor from the GLScreen + jlscript = raw""" + using GLMakie + fig, ax, pl = scatter(1:2, 3:4) + screen = display(GLMakie.Screen(visible = false), fig) + print(Int(screen.scalefactor[])) + """ + cmd = ``` + $(Base.julia_cmd()) + --project=$(Base.active_project()) + --eval $jlscript + ``` + scalefactor = readchomp(cmd) + @test scalefactor == "2" + finally + # cleanup: kill the daemon before continuing with more tests + kill(xsettingsd) + end + else + @test_broken hasxrdb && inxvfb + end + else + @test_broken Sys.islinux() + end + + GLMakie.closeall() +end diff --git a/MakieCore/src/attributes.jl b/MakieCore/src/attributes.jl index 591cecaa3f7..aecc9cedfc2 100644 --- a/MakieCore/src/attributes.jl +++ b/MakieCore/src/attributes.jl @@ -59,7 +59,15 @@ function Base.deepcopy(attributes::Attributes) end Base.filter(f, x::Attributes) = Attributes(filter(f, attributes(x))) -Base.empty!(x::Attributes) = (empty!(attributes(x)); x) +function Base.empty!(x::Attributes) + attr = attributes(x) + for (key, obs) in attr + Observables.clear(obs) + end + empty!(attr) + return x +end + Base.length(x::Attributes) = length(attributes(x)) function Base.merge!(target::Attributes, args::Attributes...) @@ -79,6 +87,13 @@ function Base.getproperty(x::Union{Attributes, AbstractPlot}, key::Symbol) end end +function Base.setproperty!(x::Union{Attributes,AbstractPlot}, key::Symbol, value::NamedTuple) + x[key] = Attributes(value) +end +function Base.setindex!(x::Attributes, value::NamedTuple, key::Symbol) + return x[key] = Attributes(value) +end + function Base.setproperty!(x::Union{Attributes, AbstractPlot}, key::Symbol, value) if hasfield(typeof(x), key) setfield!(x, key, value) @@ -172,12 +187,12 @@ Base.get(x::AttributeOrPlot, key::Symbol, default) = get(()-> default, x, key) # This is a bit confusing, since for a plot it returns the attribute from the arguments # and not a plot for integer indexing. But, we want to treat plots as "atomic" # so from an interface point of view, one should assume that a plot doesn't contain subplots -# Combined plots break this assumption in some way, but the way to look at it is, -# that the plots contained in a Combined plot are not subplots, but _are_ actually +# Plot plots break this assumption in some way, but the way to look at it is, +# that the plots contained in a Plot plot are not subplots, but _are_ actually # the plot itself. Base.getindex(plot::AbstractPlot, idx::Integer) = plot.converted[idx] Base.getindex(plot::AbstractPlot, idx::UnitRange{<:Integer}) = plot.converted[idx] -Base.setindex!(plot::AbstractPlot, value, idx::Integer) = (plot.input_args[idx][] = value) +Base.setindex!(plot::AbstractPlot, value, idx::Integer) = (plot.args[idx][] = value) Base.length(plot::AbstractPlot) = length(plot.converted) function Base.getindex(x::AbstractPlot, key::Symbol) @@ -249,14 +264,15 @@ function get_attribute(dict, key, default=nothing) end function merge_attributes!(input::Attributes, theme::Attributes) - for (key, value) in theme + for (key, value) in attributes(theme) if !haskey(input, key) input[key] = value else current_value = input[key] - if value isa Attributes && current_value isa Attributes + tvalue = to_value(value) + if tvalue isa Attributes && current_value isa Attributes # if nested attribute, we merge recursively - merge_attributes!(current_value, value) + merge_attributes!(current_value, tvalue) end # we're good! input already has a value, can ignore theme end diff --git a/MakieCore/src/basic_plots.jl b/MakieCore/src/basic_plots.jl index 99381416a82..ecf984c9971 100644 --- a/MakieCore/src/basic_plots.jl +++ b/MakieCore/src/basic_plots.jl @@ -79,16 +79,17 @@ end """ ### 3D shading attributes -- `shading = true` enables lighting. -- `diffuse::Vec3f = Vec3f(0.4)` sets how strongly the red, green and blue channel react to diffuse (scattered) light. -- `specular::Vec3f = Vec3f(0.2)` sets how strongly the object reflects light in the red, green and blue channels. +- `shading = automatic` sets the lighting algorithm used. Options are `NoShading` (no lighting), `FastShading` (AmbientLight + PointLight) or `MultiLightShading` (Multiple lights, GLMakie only). Note that this does not affect RPRMakie. +- `diffuse::Vec3f = Vec3f(1.0)` sets how strongly the red, green and blue channel react to diffuse (scattered) light. +- `specular::Vec3f = Vec3f(0.4)` sets how strongly the object reflects light in the red, green and blue channels. - `shininess::Real = 32.0` sets how sharp the reflection is. +- `backlight::Float32 = 0f0` sets a weight for secondary light calculation with inverted normals. - `ssao::Bool = false` adjusts whether the plot is rendered with ssao (screen space ambient occlusion). Note that this only makes sense in 3D plots and is only applicable with `fxaa = true`. """ function shading_attributes!(attr) - attr[:shading] = true - attr[:diffuse] = 0.4 - attr[:specular] = 0.2 + attr[:shading] = automatic + attr[:diffuse] = 1.0 + attr[:specular] = 0.4 attr[:shininess] = 32.0f0 attr[:backlight] = 0f0 attr[:ssao] = false @@ -121,7 +122,7 @@ calculated_attributes!(plot::T) where T = calculated_attributes!(T, plot) image(x, y, image) image(image) -Plots an image on range `x, y` (defaults to dimensions). +Plots an image on a rectangle bounded by `x` and `y` (defaults to size of image). ## Attributes @@ -146,7 +147,8 @@ end heatmap(x, y, values) heatmap(values) -Plots a heatmap as an image on `x, y` (defaults to interpretation as dimensions). +Plots a heatmap as a collection of rectangles centered at `x[i], y[j]` with +colors derived from `values[i, j]`. (Defaults to `x, y = axes(values)`.) ## Attributes @@ -215,11 +217,9 @@ end surface(x, y, z) surface(z) -Plots a surface, where `(x, y)` define a grid whose heights are the entries in `z`. +Plots a surface, where `(x, y)` define a grid whose heights are the entries in `z`. `x` and `y` may be `Vectors` which define a regular grid, **or** `Matrices` which define an irregular grid. -`Surface` has the conversion trait `ContinuousSurface <: SurfaceLike`. - ## Attributes ### Specific to `Surface` @@ -467,6 +467,7 @@ Plots one or multiple texts passed via the `text` keyword. - `glowwidth::Real = 0` sets the size of a glow effect around the marker. - `glowcolor::Union{Symbol, <:Colorant} = (:black, 0)` sets the color of the glow effect. - `word_wrap_with::Real = -1` specifies a linewidth limit for text. If a word overflows this limit, a newline is inserted before it. Negative numbers disable word wrapping. +- `transform_marker::Bool = false` controls whether the model matrix (without translation) applies to the glyph itself, rather than just the positions. (If this is true, `scale!` and `rotate!` will affect the text glyphs.) $(Base.Docs.doc(colormap_attributes!)) @@ -488,7 +489,7 @@ $(Base.Docs.doc(MakieCore.generic_plot_attributes!)) justification = automatic, lineheight = 1.0, markerspace = :pixel, - + transform_marker = false, offset = (0.0, 0.0), word_wrap_width = -1, ) @@ -538,7 +539,7 @@ $(Base.Docs.doc(MakieCore.generic_plot_attributes!)) strokewidth = theme(scene, :patchstrokewidth), linestyle = nothing, - shading = false, + shading = NoShading, fxaa = true, cycle = [:color => :patchcolor], @@ -572,16 +573,11 @@ end colorscale = identity, quality = 32, - inspectable = theme(scene, :inspectable), markerspace = :pixel, - - diffuse=0.4, - specular=0.2, - shininess=32.0f0, - ssao = false ) generic_plot_attributes!(attr) + shading_attributes!(attr) colormap_attributes!(attr, theme(scene, :colormap)) attr[:fxaa] = automatic diff --git a/MakieCore/src/conversion.jl b/MakieCore/src/conversion.jl index 40d3f36839d..2b2b9635d2d 100644 --- a/MakieCore/src/conversion.jl +++ b/MakieCore/src/conversion.jl @@ -29,19 +29,74 @@ struct NoConversion <: ConversionTrait end # No conversion by default conversion_trait(::Type) = NoConversion() +conversion_trait(T::Type, args...) = conversion_trait(T) + convert_arguments(::NoConversion, args...) = args +""" + PointBased() <: ConversionTrait + +Plots with the `PointBased` trait convert their input data to a +`Vector{Point{D, Float32}}`. +""" struct PointBased <: ConversionTrait end conversion_trait(::Type{<: XYBased}) = PointBased() conversion_trait(::Type{<: Text}) = PointBased() -abstract type SurfaceLike <: ConversionTrait end +""" + GridBased <: ConversionTrait + +GridBased is an abstract conversion trait for data that exists on a grid. + +Child types: [`VertexGrid`](@ref), [`CellGrid`](@ref) +See also: [`ImageLike`](@ref) +Used for: Scatter, Lines +""" +abstract type GridBased <: ConversionTrait end + +""" + VertexGrid() <: GridBased <: ConversionTrait + +Plots with the `VertexGrid` trait convert their input data to +`(xs::Vector{Float32}, ys::Vector{Float32}, zs::Matrix{Float32})` such that +`(length(xs), length(ys)) == size(zs)`, or +`(xs::Matrix{Float32}, ys::Matrix{Float32}, zs::Matrix{Float32})` such that +`size(xs) == size(ys) == size(zs)`. -struct ContinuousSurface <: SurfaceLike end -conversion_trait(::Type{<: Union{Surface, Image}}) = ContinuousSurface() +See also: [`CellGrid`](@ref), [`ImageLike`](@ref) +Used for: Surface +""" +struct VertexGrid <: GridBased end +conversion_trait(::Type{<: Surface}) = VertexGrid() + +""" + CellGrid() <: GridBased <: ConversionTrait + +Plots with the `CellGrid` trait convert their input data to +`(xs::Vector{Float32}, ys::Vector{Float32}, zs::Matrix{Float32})` such that +`(length(xs), length(ys)) == size(zs) .+ 1`. After the conversion the x and y +values represent the edges of cells corresponding to z values. + +See also: [`VertexGrid`](@ref), [`ImageLike`](@ref) +Used for: Heatmap +""" +struct CellGrid <: GridBased end +conversion_trait(::Type{<: Heatmap}) = CellGrid() + +""" + ImageLike() <: ConversionTrait + +Plots with the `ImageLike` trait convert their input data to +`(xs::Interval, ys::Interval, zs::Matrix{Float32})` where xs and ys mark the +limits of a quad containing zs. + +See also: [`CellGrid`](@ref), [`VertexGrid`](@ref) +Used for: Image +""" +struct ImageLike <: ConversionTrait end +conversion_trait(::Type{<: Image}) = ImageLike() +# Rect2f(xmin, ymin, xmax, ymax) -struct DiscreteSurface <: SurfaceLike end -conversion_trait(::Type{<: Heatmap}) = DiscreteSurface() struct VolumeLike <: ConversionTrait end conversion_trait(::Type{<: Volume}) = VolumeLike() diff --git a/MakieCore/src/recipes.jl b/MakieCore/src/recipes.jl index e0cc89b5cb9..5532d000c05 100644 --- a/MakieCore/src/recipes.jl +++ b/MakieCore/src/recipes.jl @@ -1,7 +1,7 @@ not_implemented_for(x) = error("Not implemented for $(x). You might want to put: `using Makie` into your code!") to_func_name(x::Symbol) = Symbol(lowercase(string(x))) -# Fallback for Combined ... +# Fallback for Plot ... # Will get overloaded by recipe Macro plotsym(x) = :plot @@ -9,37 +9,34 @@ function func2string(func::F) where F <: Function string(F.name.mt.name) end -plotfunc(::Combined{F}) where F = F +plotfunc(::Plot{F}) where F = F plotfunc(::Type{<: AbstractPlot{Func}}) where Func = Func plotfunc(::T) where T <: AbstractPlot = plotfunc(T) plotfunc(f::Function) = f func2type(x::T) where T = func2type(T) func2type(x::Type{<: AbstractPlot}) = x -func2type(f::Function) = Combined{f} +func2type(f::Function) = Plot{f} plotkey(::Type{<: AbstractPlot{Typ}}) where Typ = Symbol(lowercase(func2string(Typ))) plotkey(::T) where T <: AbstractPlot = plotkey(T) plotkey(::Nothing) = :scatter plotkey(any) = nothing -""" - default_plot_signatures(funcname, funcname!, PlotType) -Creates all the different overloads for `funcname` that need to be supported for the plotting frontend! -Since we add all these signatures to different functions, we make it reusable with this function. -The `Core.@__doc__` macro transfers the docstring given to the Recipe into the functions. -""" -function default_plot_signatures(funcname, funcname!, PlotType) - quote - Core.@__doc__ function ($funcname)(args...; attributes...) - plot($PlotType, args...; attributes...) - end - - Core.@__doc__ function ($funcname!)(args...; attributes...) - plot!($PlotType, args...; attributes...) - end - end -end + +argtypes(::T) where {T <: Tuple} = T + +function create_axis_like end +function create_axis_like! end +function figurelike_return end +function figurelike_return! end + +function _create_plot end +function _create_plot! end + + +plot(args...; kw...) = _create_plot(plotfunc(plottype(map(to_value, args)...)), Dict{Symbol, Any}(kw), args...) +plot!(args...; kw...) = _create_plot!(plotfunc(plottype(map(to_value, args)...)), Dict{Symbol, Any}(kw), args...) """ Each argument can be named for a certain plot type `P`. Falls back to `arg1`, `arg2`, etc. @@ -53,7 +50,7 @@ function argument_names(::Type{<:AbstractPlot}, num_args::Integer) ntuple(i -> Symbol("arg$i"), num_args) end -# Since we can use Combined like a scene in some circumstances, we define this alias +# Since we can use Plot like a scene in some circumstances, we define this alias theme(x::SceneLike, args...) = theme(x.parent, args...) theme(x::AbstractScene) = x.theme theme(x::AbstractScene, key; default=nothing) = deepcopy(get(x.theme, key, default)) @@ -103,9 +100,9 @@ We use an example to show how this works: This macro expands to several things. Firstly a type definition: - const MyPlot{ArgTypes} = Combined{myplot, ArgTypes} + const MyPlot{ArgTypes} = Plot{myplot, ArgTypes} -The type parameter of `Combined` contains the function instead of e.g. a +The type parameter of `Plot` contains the function instead of e.g. a symbol. This way the mapping from `MyPlot` to `myplot` is safer and simpler. (The downside is we always need a function `myplot` - TODO: is this a problem?) @@ -172,9 +169,10 @@ macro recipe(theme_func, Tsym::Symbol, args::Symbol...) funcname = esc(funcname_sym) expr = quote $(funcname)() = not_implemented_for($funcname) - const $(PlotType){$(esc(:ArgType))} = Combined{$funcname,$(esc(:ArgType))} + const $(PlotType){$(esc(:ArgType))} = Plot{$funcname,$(esc(:ArgType))} $(MakieCore).plotsym(::Type{<:$(PlotType)}) = $(QuoteNode(Tsym)) - $(default_plot_signatures(funcname, funcname!, PlotType)) + Core.@__doc__ ($funcname)(args...; kw...) = _create_plot($funcname, Dict{Symbol, Any}(kw), args...) + ($funcname!)(args...; kw...) = _create_plot!($funcname, Dict{Symbol, Any}(kw), args...) $(MakieCore).default_theme(scene, ::Type{<:$PlotType}) = $(esc(theme_func))(scene) export $PlotType, $funcname, $funcname! end @@ -190,25 +188,37 @@ macro recipe(theme_func, Tsym::Symbol, args::Symbol...) expr end -# Register plot / plot! using the Any type as PlotType. -# This is done so that plot(args...) / plot!(args...) can by default go -# through a pipeline where the appropriate PlotType is determined -# from the input arguments themselves. -eval(default_plot_signatures(:plot, :plot!, :Any)) - """ -Returns the Combined type that represents the signature of `args`. + Plot(args::Vararg{<:DataType,N}) + +Returns the Plot type that represents the signature of `args`. +Example: + +```julia +Plot(Vector{Point2f}) == Plot{plot, Tuple{<:Vector{Point2f}}} +``` +This can be used to more conveniently create recipes for `plot(mytype)` without the recipe macro: + +```julia +struct MyType ... end + +function Makie.plot!(plot::Plot(MyType)) + ... +end + +plot(MyType(...)) +``` """ -function Plot(args::Vararg{Any,N}) where {N} - Combined{Any,<:Tuple{args...}} +function Plot(args::Vararg{<:DataType,N}) where {N} + Plot{plot, <:Tuple{args...}} end -Base.@pure function Plot(::Type{T}) where {T} - Combined{Any,<:Tuple{T}} +function Plot(::Type{T}) where {T} + Plot{plot, <:Tuple{T}} end -Base.@pure function Plot(::Type{T1}, ::Type{T2}) where {T1,T2} - Combined{Any,<:Tuple{T1,T2}} +function Plot(::Type{T1}, ::Type{T2}) where {T1,T2} + Plot{plot, <:Tuple{T1,T2}} end """ @@ -221,4 +231,4 @@ e.g.: plottype(x::Array{<: AbstractFloat, 3}) = Volume ``` """ -plottype(plot_args...) = Combined{Any, Tuple{typeof.(to_value.(plot_args))...}} # default to dispatch to type recipes! +plottype(plot_args...) = Plot{plot, Tuple{map(typeof, plot_args)...}} # default to dispatch to type recipes! diff --git a/MakieCore/src/types.jl b/MakieCore/src/types.jl index 630de518620..e80345d339c 100644 --- a/MakieCore/src/types.jl +++ b/MakieCore/src/types.jl @@ -50,23 +50,41 @@ struct Attributes attributes::Dict{Symbol, Observable} end -struct Combined{Typ, T} <: ScenePlot{Typ} - parent::SceneLike - transformation::Transformable +""" + Plot{PlotFunc}(args::Tuple, kw::Dict{Symbol, Any}) + +Creates a Plot corresponding to the recipe function `PlotFunc`. +Each recipe defines an alias for `Plot{PlotFunc}`. +Example: +```julia +const Scatter = Plot{scatter} # defined in the scatter recipe +Plot{scatter}((1:4,), Dict{Symbol, Any}(:color => :red)) isa Scatter +# Same as: +Scatter((1:4,), Dict{Symbol, Any}(:color => :red)) +``` +""" +mutable struct Plot{PlotFunc, T} <: ScenePlot{PlotFunc} + transformation::Union{Nothing, Transformable} + + # Unprocessed arguments directly from the user command e.g. `plot(args...; kw...)`` + kw::Dict{Symbol,Any} + args::Vector{Any} + + converted::NTuple{N,Observable} where {N} + # Converted and processed arguments attributes::Attributes - input_args::Tuple - converted::Tuple - plots::Vector{AbstractPlot} + + plots::Vector{Plot} deregister_callbacks::Vector{Observables.ObserverFunction} - function Combined{Typ, T}( - parent::SceneLike, transformation::Transformable, attributes::Attributes, - input_args::Tuple, converted::Tuple, plots::Vector{AbstractPlot}) where {Typ, T} - return new(parent, transformation, attributes, input_args, converted, plots, + parent::Union{AbstractScene,Plot} + + function Plot{Typ,T}(kw::Dict{Symbol, Any}, args::Vector{Any}, converted::NTuple{N, Observable}) where {Typ,T,N} + return new{Typ,T}(nothing, kw, args, converted, Attributes(), Plot[], Observables.ObserverFunction[]) end end -function Base.show(io::IO, plot::Combined) +function Base.show(io::IO, plot::Plot) print(io, typeof(plot)) end @@ -117,3 +135,9 @@ end Billboard() = Billboard(0f0) Billboard(angle::Real) = Billboard(Float32(angle)) Billboard(angles::Vector) = Billboard(Float32.(angles)) + +@enum ShadingAlgorithm begin + NoShading + FastShading + MultiLightShading +end diff --git a/NEWS.md b/NEWS.md index 6f37b797530..6879ced31ef 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,17 @@ # News +## beta + +- GLMakie has gained support for HiDPI (aka Retina) screens. + This also enables saving images with higher resolution than screen pixel dimensions. + [#2544](https://github.com/MakieOrg/Makie.jl/pull/2544) +- Fixed an issue where NaN was interpreted as zero when rendering `surface` through CairoMakie. [#2598](https://github.com/MakieOrg/Makie.jl/pull/2598) +- Improved 3D camera handling, hotkeys and functionality [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) +- Refactored the `SurfaceLike` family of traits into `VertexBasedGrid`, `CellGrid` and `ImageLike`. [#3106](https://github.com/MakieOrg/Makie.jl/pull/3106) +- Added `shading = :verbose` in GLMakie to allow for multiple light sources. Also added more light types, fixed light directions for the previous lighting model (now `shading = :fast`) and adjusted `backlight` to affect normals. [#3246](https://github.com/MakieOrg/Makie.jl/pull/3246) +- Deprecated the `resolution` keyword in favor of `size` to reflect that this value is not a pixel resolution anymore [#3343](https://github.com/MakieOrg/Makie.jl/pull/3343). +- Changed the glyph used for negative numbers in tick labels from hyphen to minus [#3379](https://github.com/MakieOrg/Makie.jl/pull/3379). + ## master - Added `cornerradius` attribute to `Box` for rounded corners [#3346](https://github.com/MakieOrg/Makie.jl/pull/3346). @@ -19,8 +31,8 @@ ## v0.19.10 -- Fix bugs with Colorbar in recipes, add new API for creating a recipe colorbar and introduce experimental support for Categorical colormaps [#3090](https://github.com/MakieOrg/Makie.jl/pull/3090). -- Add experimental Datashader implementation [#2883](https://github.com/MakieOrg/Makie.jl/pull/2883). +- Fixed bugs with Colorbar in recipes, add new API for creating a recipe colorbar and introduce experimental support for Categorical colormaps [#3090](https://github.com/MakieOrg/Makie.jl/pull/3090). +- Added experimental Datashader implementation [#2883](https://github.com/MakieOrg/Makie.jl/pull/2883). - [Breaking] Changed the default order Polar arguments to (theta, r). [#3154](https://github.com/MakieOrg/Makie.jl/pull/3154) - General improvements to `PolarAxis`: full rlimtis & thetalimits, more controls and visual tweaks. See pr for more details.[#3154](https://github.com/MakieOrg/Makie.jl/pull/3154) @@ -64,7 +76,7 @@ - Fixed DataInspector interaction with transformations [#3002](https://github.com/MakieOrg/Makie.jl/pull/3002) - Added option `WGLMakie.activate!(resize_to_body=true)`, to make plots resize to the VSCode plotpane. Resizes to the HTML body element, so may work outside VSCode [#3044](https://github.com/MakieOrg/Makie.jl/pull/3044), [#3042](https://github.com/MakieOrg/Makie.jl/pull/3042). - 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) +- Fixed 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). - Scaled `errorbar` whiskers and `bracket` correctly with transformations [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012). diff --git a/Project.toml b/Project.toml index a3309e74cea..feef68c43ec 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Makie" uuid = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" authors = ["Simon Danisch", "Julius Krumbiegel"] -version = "0.19.12" +version = "0.20.0" [deps] Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" @@ -18,6 +18,7 @@ DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" FFMPEG_jll = "b22a6f82-2f65-5046-a5b2-351ab43fb4e5" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +FilePaths = "8fc22ac5-c921-52a6-82fd-178b2807b824" FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" Formatting = "59287772-0a20-5a39-b81b-1366585eb4c0" FreeType = "b38be410-82b0-50bf-ab77-7b57e271db43" @@ -75,7 +76,7 @@ Formatting = "0.4" FreeType = "3.0, 4.0" FreeTypeAbstraction = "0.10" GeometryBasics = "0.4.2" -GridLayoutBase = "0.9" +GridLayoutBase = "0.10" ImageIO = "0.2, 0.3, 0.4, 0.5, 0.6" IntervalSets = "0.3, 0.4, 0.5, 0.6, 0.7" Isoband = "0.1" @@ -84,7 +85,7 @@ LaTeXStrings = "1.2" MacroTools = "0.5" MakieCore = "=0.6.9" MathTeXEngine = "0.5" -Observables = "0.5.3" +Observables = "0.5.5" OffsetArrays = "1" Packing = "0.5" PlotUtils = "1" diff --git a/README.md b/README.md index b425927bfe1..7c83c351647 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ The following examples are supposed to be self-explanatory. For further informat x = 1:0.1:10 fig = lines(x, x.^2; label = "Parabola", axis = (; xlabel = "x", ylabel = "y", title ="Title"), - figure = (; resolution = (800,600), fontsize = 22)) + figure = (; size = (800,600), fontsize = 22)) axislegend(; position = :lt) save("./assets/parabola.png", fig) fig @@ -168,7 +168,7 @@ with_theme(palette = (; patchcolor = cgrad(cmap, alpha=0.45))) do band!(x, sin.(x), approx .+= -x .^ 7 / 5040; label = L"n = 3") limits!(-3.8, 3.8, -1.5, 1.5) axislegend(; position = :ct, backgroundcolor = (:white, 0.75), framecolor = :orange) - save("./assets/approxsin.png", fig, resolution = (800, 600)) + save("./assets/approxsin.png", fig, size = (800, 600)) fig end ``` diff --git a/RPRMakie/Project.toml b/RPRMakie/Project.toml index 55eec6498d0..b02bc403b09 100644 --- a/RPRMakie/Project.toml +++ b/RPRMakie/Project.toml @@ -1,7 +1,7 @@ name = "RPRMakie" uuid = "22d9f318-5e34-4b44-b769-6e3734a732a6" authors = ["Simon Danisch"] -version = "0.5.12" +version = "0.6.0" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" @@ -16,7 +16,7 @@ RadeonProRender = "27029320-176d-4a42-b57d-56729d2ad457" Colors = "0.9, 0.10, 0.11, 0.12" FileIO = "1.6" GeometryBasics = "0.4.1" -Makie = "=0.19.12" +Makie = "=0.20.0" RadeonProRender = "0.3.0" julia = "1.3" LinearAlgebra = "1.0, 1.6" diff --git a/RPRMakie/examples/bars.jl b/RPRMakie/examples/bars.jl index a6be9abe0e6..993be72e3eb 100644 --- a/RPRMakie/examples/bars.jl +++ b/RPRMakie/examples/bars.jl @@ -3,7 +3,7 @@ using Colors, FileIO, ImageShow using Colors: N0f8 RPRMakie.activate!(plugin=RPR.Northstar, resource=RPR.GPU0) -fig = Figure(; resolution=(800, 600), fontsize=26) +fig = Figure(; size=(800, 600), fontsize=26) radiance = 10000 lights = [EnvironmentLight(0.5, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(0, 0, 20), RGBf(radiance, radiance, radiance))] diff --git a/RPRMakie/examples/eart_topographie_sphere.jl b/RPRMakie/examples/eart_topographie_sphere.jl index 6bef6b10abc..e02ee0a2fc9 100644 --- a/RPRMakie/examples/eart_topographie_sphere.jl +++ b/RPRMakie/examples/eart_topographie_sphere.jl @@ -39,7 +39,7 @@ xetopo, yetopo, zetopo = lonlat3D(lonext, lat, dataext) begin r = 30 lights = [PointLight(Vec3f(2, 1, 3), RGBf(r, r, r))] - fig = Figure(; resolution=(1200, 1200), backgroundcolor=:black) + fig = Figure(; size=(1200, 1200), backgroundcolor=:black) ax = LScene(fig[1, 1]; show_axis=false)#, scenekw=(lights=lights,)) pltobj = surface!(ax, xetopo, yetopo, zetopo; color=dataext, colormap=:hot, colorrange=(-6000, 5000)) cam = cameracontrols(ax.scene) diff --git a/RPRMakie/examples/earth_topography.jl b/RPRMakie/examples/earth_topography.jl index 7f52cc43ca2..ceaf99ef70a 100644 --- a/RPRMakie/examples/earth_topography.jl +++ b/RPRMakie/examples/earth_topography.jl @@ -28,7 +28,7 @@ function glow_material(data_normed) end RPRMakie.activate!(iterations=32, plugin=RPR.Northstar) -fig = Figure(; resolution=(2000, 800)) +fig = Figure(; size=(2000, 800)) radiance = 30000 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(0, 100, 100), RGBf(radiance, radiance, radiance))] diff --git a/RPRMakie/examples/lego.jl b/RPRMakie/examples/lego.jl index cb364038271..fa9956c947d 100644 --- a/RPRMakie/examples/lego.jl +++ b/RPRMakie/examples/lego.jl @@ -69,7 +69,7 @@ lights = [ EnvironmentLight(1.5, rotl90(load(assetpath("sunflowers_1k.hdr"))')), PointLight(Vec3f(50, 0, 200), RGBf(radiance, radiance, radiance*1.1)), ] -s = Scene(resolution=(500, 500), lights=lights) +s = Scene(size=(500, 500), lights=lights) cam3d!(s) c = cameracontrols(s) diff --git a/RPRMakie/examples/lines.jl b/RPRMakie/examples/lines.jl index b3f2624e7f9..2fed509dc2d 100644 --- a/RPRMakie/examples/lines.jl +++ b/RPRMakie/examples/lines.jl @@ -12,7 +12,7 @@ function box!(ax, size) end begin - fig = Figure(; resolution=(1000, 1000)) + fig = Figure(; size=(1000, 1000)) 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) diff --git a/RPRMakie/examples/material_x.jl b/RPRMakie/examples/material_x.jl index 415f87c7e7a..757fd825c90 100644 --- a/RPRMakie/examples/material_x.jl +++ b/RPRMakie/examples/material_x.jl @@ -7,7 +7,7 @@ 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)) + fig = Figure(; size=(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 diff --git a/RPRMakie/examples/materials.jl b/RPRMakie/examples/materials.jl index ccdcb2209d7..80f7d9a0f2e 100644 --- a/RPRMakie/examples/materials.jl +++ b/RPRMakie/examples/materials.jl @@ -6,7 +6,7 @@ img = begin radiance = 500 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] - fig = Figure(; resolution=(1500, 700)) + fig = Figure(; size=(1500, 700)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) screen = RPRMakie.Screen(ax.scene; plugin=RPR.Northstar, iterations=1000) diff --git a/RPRMakie/examples/opengl_interop.jl b/RPRMakie/examples/opengl_interop.jl index 40e800d9f30..6141b833658 100644 --- a/RPRMakie/examples/opengl_interop.jl +++ b/RPRMakie/examples/opengl_interop.jl @@ -11,7 +11,7 @@ radiance = 500 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] -fig = Figure(; resolution=(1500, 1000)) +fig = Figure(; size=(1500, 1000)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) screen = RPRMakie.Screen(size(ax.scene); plugin=RPR.Northstar, resource=RPR.RPR_CREATION_FLAGS_ENABLE_GPU0) material = RPR.UberMaterial(screen.matsys) diff --git a/RPRMakie/examples/sea_cables.jl b/RPRMakie/examples/sea_cables.jl index 5e1185b3653..0f78515347f 100644 --- a/RPRMakie/examples/sea_cables.jl +++ b/RPRMakie/examples/sea_cables.jl @@ -50,7 +50,7 @@ earth_img = load(Downloads.download("https://upload.wikimedia.org/wikipedia/comm # the actual plot ! RPRMakie.activate!(; iterations=100) scene = with_theme(theme_dark()) do - fig = Figure(; resolution=(1000, 1000)) + fig = Figure(; size=(1000, 1000)) radiance = 30 lights = [EnvironmentLight(0.5, load(RPR.assetpath("starmap_4k.tif"))), PointLight(Vec3f(1, 1, 3), RGBf(radiance, radiance, radiance))] diff --git a/RPRMakie/examples/volume.jl b/RPRMakie/examples/volume.jl index 7c4a86ddb63..c44b59b7dc1 100644 --- a/RPRMakie/examples/volume.jl +++ b/RPRMakie/examples/volume.jl @@ -8,7 +8,7 @@ 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)) +fig = Figure(; size=(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 74bf5fc7d4d..b83ae20599e 100644 --- a/RPRMakie/src/RPRMakie.jl +++ b/RPRMakie/src/RPRMakie.jl @@ -34,11 +34,16 @@ function ScreenConfig(iterations::Int, max_recursion::Int, render_resource, rend ) end + + include("scene.jl") include("lines.jl") include("meshes.jl") include("volume.jl") +Makie.apply_screen_config!(screen::RPRMakie.Screen, ::RPRMakie.ScreenConfig, args...) = screen +Base.empty!(::RPRMakie.Screen) = nothing + """ RPRMakie.activate!(; screen_config...) diff --git a/RPRMakie/src/meshes.jl b/RPRMakie/src/meshes.jl index e5142389c8b..43bb3862d17 100644 --- a/RPRMakie/src/meshes.jl +++ b/RPRMakie/src/meshes.jl @@ -135,8 +135,8 @@ function to_rpr_object(context, matsys, scene, plot::Makie.Surface) z = plot[3] function grid(x, y, z, trans) + space = to_value(get(plot, :space, :data)) g = map(CartesianIndices(z)) do i - space = to_value(get(plot, :space, :data)) p = Point3f(Makie.get_dim(x, i, 1, size(z)), Makie.get_dim(y, i, 2, size(z)), z[i]) return Makie.apply_transform(trans, p, space) end diff --git a/RPRMakie/src/scene.jl b/RPRMakie/src/scene.jl index 26af43e6fa7..1e85771e6e2 100644 --- a/RPRMakie/src/scene.jl +++ b/RPRMakie/src/scene.jl @@ -26,7 +26,7 @@ function to_rpr_object(context, matsys, scene, plot) return nothing end -function insert_plots!(context, matsys, scene, mscene::Makie.Scene, @nospecialize(plot::Combined)) +function insert_plots!(context, matsys, scene, mscene::Makie.Scene, @nospecialize(plot::Plot)) if isempty(plot.plots) # if no plots inserted, this truly is an atomic object = to_rpr_object(context, matsys, mscene, plot) if !isnothing(object) @@ -43,18 +43,79 @@ function insert_plots!(context, matsys, scene, mscene::Makie.Scene, @nospecializ end end -function to_rpr_light(context::RPR.Context, light::Makie.PointLight) +to_rpr_light(ctx, rpr_scene, light, scene) = to_rpr_light(ctx, rpr_scene, light) + +# TODO attenuation +function to_rpr_light(context::RPR.Context, matsys, light::Makie.PointLight) pointlight = RPR.PointLight(context) map(light.position) do pos transform!(pointlight, Makie.translationmatrix(pos)) end - map(light.radiance) do r - setradiantpower!(pointlight, red(r), green(r), blue(r)) + map(light.color) do c + setradiantpower!(pointlight, red(c), green(c), blue(c)) end return pointlight end -function to_rpr_light(context::RPR.Context, light::Makie.AmbientLight) +# TODO: Move to RadeonProRender.jl +function RPR.RPR.rprContextCreateSpotLight(context) + out_light = Ref{RPR.rpr_light}() + RPR.RPR.rprContextCreateSpotLight(context, out_light) + return out_light[] +end + +function to_rpr_light(context::RPR.Context, rpr_scene, light::Makie.DirectionalLight, scene) + directionallight = RPR.DirectionalLight(context) + map(light.direction) do dir + if light.camera_relative + T = inv(scene.camera.view[][Vec(1,2,3), Vec(1,2,3)]) + dir = normalize(T * dir) + else + dir = normalize(dir) + end + quart = Makie.rotation_between(dir, Vec3f(0,0,-1)) + transform!(directionallight, Makie.rotationmatrix4(quart)) + end + map(light.color) do c + setradiantpower!(directionallight, red(c), green(c), blue(c)) + end + return directionallight +end + +function to_rpr_light(context::RPR.Context, rpr_scene, light::Makie.RectLight) + mesh = lift(light.position, light.u1, light.u2) do center, u1, u2 + pos = center - 0.5u1 - 0.5u2 + points = Point3f[pos, pos + u1, pos + u1 + u2, pos + u2] + faces = [GLTriangleFace(1, 2, 3), GLTriangleFace(1, 3, 4)] + return GeometryBasics.Mesh(points, faces) + end + rpr_mesh = RPR.Shape(context, mesh[]) + env_img = fill(light.color[], 1, 1) + img = RPR.Image(context, env_img) + env_light = RPR.EnvironmentLight(context) + set!(env_light, img) + setintensityscale!(env_light, 0.1) + # TODO, this doesn't seem to properly create a rectangular portal -.- + setportal!(rpr_scene, env_light, rpr_mesh) + return env_light +end + +function to_rpr_light(context::RPR.Context, rpr_scene, light::Makie.SpotLight) + spotlight = RPR.SpotLight(context) + map(light.position, light.direction) do pos, dir + quart = Makie.rotation_between(dir, Vec3f(0,0,-1)) + transform!(spotlight, Makie.translationmatrix(pos) * Makie.rotationmatrix4(quart)) + end + map(light.color) do c + setradiantpower!(spotlight, red(c), green(c), blue(c)) + end + map(light.angles) do (inner, outer) + RadeonProRender.RPR.rprSpotLightSetConeShape(spotlight, inner, outer) + end + return spotlight +end + +function to_rpr_light(context::RPR.Context, rpr_scene, light::Makie.AmbientLight) env_img = fill(light.color[], 1, 1) img = RPR.Image(context, env_img) env_light = RPR.EnvironmentLight(context) @@ -62,7 +123,7 @@ function to_rpr_light(context::RPR.Context, light::Makie.AmbientLight) return env_light end -function to_rpr_light(context::RPR.Context, light::Makie.EnvironmentLight) +function to_rpr_light(context::RPR.Context, rpr_scene, light::Makie.EnvironmentLight) env_light = RPR.EnvironmentLight(context) last_img = RPR.Image(context, light.image[]) set!(env_light, last_img) @@ -90,7 +151,7 @@ function to_rpr_scene(context::RPR.Context, matsys, mscene::Makie.Scene) RPR.rprSceneSetBackgroundImage(scene, img) end for light in mscene.lights - rpr_light = to_rpr_light(context, light) + rpr_light = to_rpr_light(context, scene, light, mscene) push!(scene, rpr_light) end @@ -192,12 +253,12 @@ function Makie.apply_screen_config!(screen::Screen, config::ScreenConfig) end function Screen(fb_size::NTuple{2,<:Integer}; screen_config...) - config = Makie.merge_screen_config(ScreenConfig, screen_config) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) return Screen(fb_size, config) end function Screen(scene::Scene; screen_config...) - config = Makie.merge_screen_config(ScreenConfig, screen_config) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) return Screen(scene, config) end diff --git a/RPRMakie/test/lines.jl b/RPRMakie/test/lines.jl index e36c4698486..9dba114232c 100644 --- a/RPRMakie/test/lines.jl +++ b/RPRMakie/test/lines.jl @@ -12,7 +12,7 @@ begin emissive = RPR.EmissiveMaterial(matsys) diffuse = RPR.DiffuseMaterial(matsys) - fig = Figure(resolution=(1000, 1000)) + fig = Figure(size=(1000, 1000)) ax = LScene(fig[1, 1], show_axis=false) for i in 4:4:12 n = i + 1 diff --git a/ReferenceTests/src/database.jl b/ReferenceTests/src/database.jl index 6a8480d2f88..090a6ada7be 100644 --- a/ReferenceTests/src/database.jl +++ b/ReferenceTests/src/database.jl @@ -29,29 +29,32 @@ macro reference_test(name, code) funcs = used_functions(code) skip = (title in SKIP_TITLES) || any(x-> x in funcs, SKIP_FUNCTIONS) return quote - t1 = time() @testset $(title) begin if $skip @test_broken false else + t1 = time() if $title in $REGISTERED_TESTS error("title must be unique. Duplicate title: $(title)") end println("running $(lpad(COUNTER[] += 1, 3)): $($title)") - Makie.set_theme!(resolution=(500, 500)) + Makie.set_theme!(; size=(500, 500), + CairoMakie=(; px_per_unit=1), + GLMakie=(; scalefactor=1, px_per_unit=1), + WGLMakie=(; scalefactor=1, px_per_unit=1)) ReferenceTests.RNG.seed_rng!() result = let $(esc(code)) end @test save_result(joinpath(RECORDING_DIR[], $title), result) push!($REGISTERED_TESTS, $title) + elapsed = round(time() - t1; digits=5) + total = Sys.total_memory() + mem = round((total - Sys.free_memory()) / 10^9; digits=3) + # TODO, write to file and create an overview in the end, similar to the benchmark results! + println("Used $(mem)gb of $(round(total / 10^9; digits=3))gb RAM, time: $(elapsed)s") end end - elapsed = round(time() - t1; digits=3) - total = Sys.total_memory() - mem = round((total - Sys.free_memory()) / 10^9; digits=3) - # TODO, write to file and create an overview in the end, similar to the benchmark results! - println("Used $(mem)gb of $(round(total / 10^9; digits=3))gb RAM, time: $(elapsed)s") end end diff --git a/ReferenceTests/src/tests/attributes.jl b/ReferenceTests/src/tests/attributes.jl index 845a1d3e7e0..5b89c3d86bd 100644 --- a/ReferenceTests/src/tests/attributes.jl +++ b/ReferenceTests/src/tests/attributes.jl @@ -27,7 +27,7 @@ end end @reference_test "shading" begin - mesh(Sphere(Point3f(0), 1f0), color=:orange, shading=false) + mesh(Sphere(Point3f(0), 1f0), color=:orange, shading=NoShading) end @reference_test "visible" begin diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index 225bc4a6249..8cc85e7e075 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -13,7 +13,7 @@ end end @reference_test "heatmap_interpolation" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) data = RNG.rand(32, 32) # the grayscale heatmap hides the problem that interpolation based on values # in GLMakie looks different than interpolation based on colors in CairoMakie @@ -110,7 +110,7 @@ end 5 8 9; ] color = [0.0, 0.0, 0.0, 0.0, -0.375, 0.0, 0.0, 0.0, 0.0] - fig, ax, meshplot = mesh(coordinates, connectivity, color=color, shading=false) + fig, ax, meshplot = mesh(coordinates, connectivity, color=color, shading=NoShading) wireframe!(ax, meshplot[1], color=(:black, 0.6), linewidth=3) fig end @@ -118,7 +118,7 @@ end @reference_test "colored triangle" begin mesh( [(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)], color=[:red, :green, :blue], - shading=false + shading=NoShading ) end @@ -360,7 +360,7 @@ end @reference_test "Simple pie chart" begin - fig = Figure(resolution=(800, 800)) + fig = Figure(size=(800, 800)) pie(fig[1, 1], 1:5, color=collect(1:5), axis=(;aspect=DataAspect())) fig end @@ -420,7 +420,7 @@ end @reference_test "space 2D" begin # This should generate a regular grid with text in a circle in a box. All # sizes and positions are scaled to be equal across all options. - fig = Figure(resolution = (700, 700)) + fig = Figure(size = (700, 700)) ax = Axis(fig[1, 1], width = 600, height = 600) spaces = (:data, :pixel, :relative, :clip) xs = [ @@ -435,7 +435,7 @@ end s = 1.5scales[i] mesh!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, - shading = false, color = :blue) + shading = NoShading, color = :blue) lines!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, linewidth = 2, color = :red) @@ -462,7 +462,7 @@ end # - (x -> data) row should have stretched circle and text ain x direction # - (not data -> data) should keep aspect ratio for mesh and lines # - (data -> x) should be slightly missaligned with (not data -> x) - fig = Figure(resolution = (700, 700)) + fig = Figure(size = (700, 700)) ax = Axis(fig[1, 1], width = 600, height = 600) spaces = (:data, :pixel, :relative, :clip) xs = [ @@ -477,7 +477,7 @@ end s = 1.5scales[i] mesh!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, - shading = false, color = :blue) + shading = NoShading, color = :blue) lines!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, linewidth = 2, color = :red) @@ -496,7 +496,7 @@ end @reference_test "Scatter & Text transformations" begin # Check that transformations apply in `space = :data` fig, ax, p = scatter(Point2f(100, 0.5), marker = 'a', markersize=50) - t = text!(Point2f(100, 0.5), text = "Test", fontsize = 50) + t = text!(Point2f(100, 0.5), text = "Test", fontsize = 50, transform_marker=true) translate!(p, -100, 0, 0) translate!(t, -100, 0, 0) @@ -506,7 +506,7 @@ end scale!(p2, 0.5, 0.5, 1) # but do act on glyphs of text - t2 = text!(ax, 1, 0, text = "Test", fontsize = 50) + t2 = text!(ax, 1, 0, text = "Test", fontsize = 50, transform_marker=true) Makie.rotate!(t2, pi/4) scale!(t2, 0.5, 0.5, 1) @@ -530,11 +530,11 @@ end end @reference_test "2D surface with explicit color" begin - surface(1:10, 1:10, ones(10, 10); color = [RGBf(x*y/100, 0, 0) for x in 1:10, y in 1:10], shading = false) + surface(1:10, 1:10, ones(10, 10); color = [RGBf(x*y/100, 0, 0) for x in 1:10, y in 1:10], shading = NoShading) end @reference_test "heatmap and image colormap interpolation" begin - f = Figure(resolution=(500, 500)) + f = Figure(size=(500, 500)) crange = LinRange(0, 255, 10) len = length(crange) img = zeros(Float32, len, len + 2) @@ -561,7 +561,7 @@ end n = 100 categorical = [false, true] scales = [exp, identity, log, log10] - fig = Figure(resolution = (500, 250)) + fig = Figure(size = (500, 250)) ax = Axis(fig[1, 1]) for (i, cat) in enumerate(categorical) for (j, scale) in enumerate(scales) @@ -580,7 +580,7 @@ end @reference_test "colormap with specific values" begin cmap = cgrad([:black,:white,:orange],[0,0.2,1]) - fig = Figure(resolution=(400,200)) + fig = Figure(size=(400,200)) ax = Axis(fig[1,1]) x = range(0,1,length=50) scatter!(fig[1,1],Point2.(x,fill(0.,50)),color=x,colormap=cmap) @@ -633,7 +633,7 @@ end @reference_test "minor grid & scales" begin data = LinRange(0.01, 0.99, 200) - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) for (i, scale) in enumerate([log10, log2, log, sqrt, Makie.logit, identity]) row, col = fldmod1(i, 2) Axis(f[row, col], yscale = scale, title = string(scale), @@ -808,7 +808,7 @@ end end @reference_test "contour labels with transform_func" begin - f = Figure(resolution = (400, 400)) + f = Figure(size = (400, 400)) a = Axis(f[1, 1], xscale = log10) xs = 10 .^ range(0, 3, length=101) ys = range(1, 4, length=101) @@ -840,7 +840,7 @@ end @reference_test "trimspine" begin with_theme(Axis = (limits = (0.5, 5.5, 0.3, 3.4), spinewidth = 8, topspinevisible = false, rightspinevisible = false)) do - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) for (i, ts) in enumerate([(true, true), (true, false), (false, true), (false, false)]) Label(f[0, i], string(ts), tellwidth = false) @@ -859,7 +859,7 @@ end end @reference_test "hexbin bin int" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) x = RNG.rand(300) y = RNG.rand(300) @@ -875,7 +875,7 @@ end end @reference_test "hexbin bin tuple" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) x = RNG.rand(300) y = RNG.rand(300) @@ -893,7 +893,7 @@ end @reference_test "hexbin two cellsizes" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) x = RNG.rand(300) y = RNG.rand(300) @@ -909,7 +909,7 @@ end end @reference_test "hexbin one cellsize" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) x = RNG.rand(300) y = RNG.rand(300) @@ -925,7 +925,7 @@ end end @reference_test "hexbin threshold" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) x = RNG.randn(100000) y = RNG.randn(100000) @@ -974,7 +974,7 @@ end end @reference_test "Rich text" begin - f = Figure(fontsize = 30, resolution = (800, 600)) + f = Figure(fontsize = 30, size = (800, 600)) ax = Axis(f[1, 1], limits = (1, 100, 0.001, 1), xscale = log10, @@ -1066,10 +1066,10 @@ end end @reference_test "LaTeXStrings linesegment offsets" begin - s = Scene(camera = campixel!, resolution = (600, 600)) + s = Scene(camera = campixel!, size = (600, 600)) for (i, (offx, offy)) in enumerate(zip([0, 20, 50], [0, 10, 30])) for (j, rot) in enumerate([0, pi/4, pi/2]) - scatter!(s, 150i, 150j) + scatter!(s, 150i, 150j, color=:black) text!(s, 150i, 150j, text = L"\sqrt{x+y}", offset = (offx, offy), rotation = rot, fontsize = 30) end @@ -1078,7 +1078,7 @@ end end @reference_test "Scalar colors from colormaps" begin - f = Figure(resolution = (600, 600)) + f = Figure(size = (600, 600)) ax = Axis(f[1, 1]) hidedecorations!(ax) hidespines!(ax) @@ -1107,7 +1107,7 @@ end # It seems like we can't define recipes in `@reference_test` yet, # so we'll have to fake a recipe's structure. - fig = Figure(resolution = (600, 600)) + fig = Figure(size = (600, 600)) # Create a recipe plot ax, plot_top = heatmap(fig[1, 1], randn(10, 10)) # Plot some recipes at the level below the contour @@ -1349,3 +1349,27 @@ end ylims!(ax2,-0.5,2.5) # need to make sure all generators are shown, and the bounding box is automatically updated fig end + +function ppu_test_plot(resolution, px_per_unit, scalefactor) + fig, ax, pl = scatter(1:4, markersize=100, color=1:4, figure=(; size=resolution), axis=(; titlesize=50, title="ppu: $px_per_unit, sf: $scalefactor")) + DataInspector(ax) + hidedecorations!(ax) + return fig +end + +@reference_test "px_per_unit and scalefactor" begin + resolution = (800, 800) + let st = nothing + @testset begin + matr = [(px, scale) for px in [0.5, 1, 2], scale in [0.5, 1, 2]] + imgs = map(matr) do (px_per_unit, scalefactor) + img = colorbuffer(ppu_test_plot(resolution, px_per_unit, scalefactor); px_per_unit=px_per_unit, scalefactor=scalefactor) + @test size(img) == (800, 800) .* px_per_unit + return img + end + fig = Figure() + st = Makie.RamStepper(fig, Makie.current_backend().Screen(fig.scene), vec(imgs), :png) + end + st + end +end diff --git a/ReferenceTests/src/tests/examples3d.jl b/ReferenceTests/src/tests/examples3d.jl index e32e9980322..507b476b459 100644 --- a/ReferenceTests/src/tests/examples3d.jl +++ b/ReferenceTests/src/tests/examples3d.jl @@ -1,7 +1,7 @@ @reference_test "Image on Geometry (Moon)" begin moon = loadasset("moon.png") - fig, ax, meshplot = mesh(Sphere(Point3f(0), 1f0), color=moon, shading=false, axis = (;show_axis=false)) + fig, ax, meshplot = mesh(Sphere(Point3f(0), 1f0), color=moon, shading=NoShading, axis = (;show_axis=false)) update_cam!(ax.scene, Vec3f(-2, 2, 2), Vec3f(0)) fig end @@ -9,7 +9,7 @@ end @reference_test "Image on Geometry (Earth)" begin earth = loadasset("earth.png") m = uv_mesh(Tesselation(Sphere(Point3f(0), 1f0), 60)) - mesh(m, color=earth, shading=false) + mesh(m, color=earth, shading=NoShading) end @reference_test "Orthographic Camera" begin @@ -29,19 +29,12 @@ end meshes = map(colormesh, rectangles) fig, ax, meshplot = mesh(merge(meshes)) scene = ax.scene - center!(scene) cam = cameracontrols(scene) - dir = widths(data_limits(scene)) ./ 2. - dir_scaled = Vec3f( - dir[1] * scene.transformation.scale[][1], - 0.0, - dir[3] * scene.transformation.scale[][2], - ) + cam.settings[:projectiontype][] = Makie.Orthographic + cam.settings.center[] = false # This would be set by update_cam!() cam.upvector[] = (0.0, 0.0, 1.0) - cam.lookat[] = minimum(data_limits(scene)) + dir_scaled - cam.eyeposition[] = (cam.lookat[][1], cam.lookat[][2] + 6.3, cam.lookat[][3]) - cam.attributes[:projectiontype][] = Makie.Orthographic - cam.zoom_mult[] = 0.61f0 + cam.lookat[] = Vec3f(0.595, 1.5, 0.5) + cam.eyeposition[] = (cam.lookat[][1], cam.lookat[][2] + 0.61, cam.lookat[][3]) update_cam!(scene, cam) fig end @@ -61,13 +54,13 @@ end rot = qrotation(Vec3f(1, 0, 0), 0.5pi) * qrotation(Vec3f(0, 1, 0), 0.7pi) meshscatter( 1:3, 1:3, fill(0, 3, 3), - marker=catmesh, color=img, markersize=1, rotation=rot, + marker=catmesh, color=img, markersize=1, rotations=rot, axis=(type=LScene, show_axis=false) ) end @reference_test "Load Mesh" begin - mesh(loadasset("cat.obj")) + mesh(loadasset("cat.obj"); color=:black) end @reference_test "Colored Mesh" begin @@ -201,7 +194,6 @@ end x = [cospi(φ) * sinpi(θ) for θ in θ, φ in φ] y = [sinpi(φ) * sinpi(θ) for θ in θ, φ in φ] z = [cospi(θ) for θ in θ, φ in φ] - RNG.rand([-1f0, 1f0], 3) pts = vec(Point3f.(x, y, z)) f, ax, p = surface(x, y, z, color=Makie.logo(), transparency=true) end @@ -282,7 +274,7 @@ end vy = -1:0.01:1 f(x, y) = (sin(x * 10) + cos(y * 10)) / 4 - scene = Scene(resolution=(500, 500), camera=cam3d!) + scene = Scene(size=(500, 500), camera=cam3d!) # One way to style the axis is to pass a nested dictionary / named tuple to it. psurf = surface!(scene, vx, vy, f) axis3d!(scene, frame = (linewidth = 2.0,)) @@ -452,8 +444,8 @@ end @reference_test "Line GIF" begin us = range(0, stop=1, length=100) - f, ax, p = linesegments(Rect3f(Vec3f(0, -1, 0), Vec3f(1, 2, 2))) - p = lines!(ax, us, sin.(us), zeros(100), linewidth=3, transparency=true) + f, ax, p = linesegments(Rect3f(Vec3f(0, -1, 0), Vec3f(1, 2, 2)); color=:black) + p = lines!(ax, us, sin.(us), zeros(100), linewidth=3, transparency=true, color=:black) lineplots = [p] Makie.translate!(p, 0, 0, 0) colors = to_colormap(:RdYlBu) @@ -533,7 +525,7 @@ end @reference_test "Depth Shift" begin # Up to some artifacts from fxaa the left side should be blue and the right red. - fig = Figure(resolution = (800, 400)) + fig = Figure(size = (800, 400)) prim = Rect3(Point3f(0), Vec3f(1)) ps = RNG.rand(Point3f, 10) .+ Point3f(0, 0, 1) @@ -591,7 +583,7 @@ end end end cam = cameracontrols(ax.scene) - cam.attributes.fov[] = 22f0 + cam.fov[] = 22f0 update_cam!(ax.scene, cam, Vec3f(0.625, 0, 3.5), Vec3f(0.625, 0, 0), Vec3f(0, 1, 0)) fig end @@ -601,7 +593,7 @@ end fig = Figure() for ax in [LScene(fig[1, 1]), Axis3(fig[1, 2])] mesh!(ax, Rect3(Point3f(-10), Vec3f(20)), color = :orange) - mesh!(ax, Rect2f(0.8, 0.1, 0.1, 0.8), space = :relative, color = :blue, shading = false) + mesh!(ax, Rect2f(0.8, 0.1, 0.1, 0.8), space = :relative, color = :blue, shading = NoShading) linesegments!(ax, Rect2f(-0.5, -0.5, 1, 1), space = :clip, color = :cyan, linewidth = 5) text!(ax, 0, 0.52, text = "Clip Space", align = (:center, :bottom), space = :clip) image!(ax, 0..40, 0..800, [x for x in range(0, 1, length=40), _ in 1:10], space = :pixel) diff --git a/ReferenceTests/src/tests/figures_and_makielayout.jl b/ReferenceTests/src/tests/figures_and_makielayout.jl index 465f6b15056..787ede722cb 100644 --- a/ReferenceTests/src/tests/figures_and_makielayout.jl +++ b/ReferenceTests/src/tests/figures_and_makielayout.jl @@ -8,7 +8,7 @@ end @reference_test "Figure with Blocks" begin - fig = Figure(resolution = (900, 900)) + fig = Figure(size = (900, 900)) ax, sc = scatter(fig[1, 1][1, 1], RNG.randn(100, 2), axis = (;title = "Random Dots", xlabel = "Time")) sc2 = scatter!(ax, RNG.randn(100, 2) .+ 2, color = :red) ll = fig[1, 1][1, 2] = Legend(fig, [sc, sc2], ["Scatter", "Other"]) @@ -26,7 +26,7 @@ end end @reference_test "Figure with boxes" begin - fig = Figure(resolution = (900, 900)) + fig = Figure(size = (900, 900)) Box(fig[1,1], color = :red, strokewidth = 3, linestyle = :solid, strokecolor = :black) Box(fig[1,2], color = (:red, 0.5), strokewidth = 3, linestyle = :dash, strokecolor = :red) Box(fig[1,3], color = :white, strokewidth = 3, linestyle = :dot, strokecolor = (:black, 0.5)) @@ -61,8 +61,8 @@ end @reference_test "Label with text wrapping" begin lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - fig = Figure(resolution = (1000, 660)) - m!(fig, lbl) = mesh!(fig.scene, lbl.layoutobservables.computedbbox, color = (:red, 0.5), shading=false) + fig = Figure(size = (1000, 660)) + m!(fig, lbl) = mesh!(fig.scene, lbl.layoutobservables.computedbbox, color = (:red, 0.5), shading=NoShading) lbl1 = Label(fig[1, 1:2], "HEADER "^10, fontsize = 40, word_wrap = true) m!(fig, lbl1) @@ -152,7 +152,7 @@ end f = Figure() ax = PolarAxis(f[1, 1]) zs = [r*cos(phi) for phi in range(0, 4pi, length=100), r in range(1, 2, length=100)] - p = surface!(ax, 0..2pi, 0..10, zs, shading = false, colormap = :coolwarm, colorrange=(-2, 2)) + p = surface!(ax, 0..2pi, 0..10, zs, shading = NoShading, colormap = :coolwarm, colorrange=(-2, 2)) rlims!(ax, 0, 11) # verify that r = 10 doesn't end up at r > 10 translate!(p, 0, 0, -200) Colorbar(f[1, 2], p) @@ -161,7 +161,7 @@ end # may fail in WGLMakie due to missing dashes @reference_test "PolarAxis scatterlines spine" begin - f = Figure(resolution = (800, 400)) + f = Figure(size = (800, 400)) ax1 = PolarAxis(f[1, 1], title = "No spine", spinevisible = false, theta_as_x = false) scatterlines!(ax1, range(0, 1, length=100), range(0, 10pi, length=100), color = 1:100) @@ -176,7 +176,7 @@ end # may fail in CairoMakie due to different text stroke handling # and in WGLMakie due to missing stroke @reference_test "PolarAxis decorations" begin - f = Figure(resolution = (400, 400), backgroundcolor = :black) + f = Figure(size = (400, 400), backgroundcolor = :black) ax = PolarAxis( f[1, 1], backgroundcolor = :black, @@ -195,7 +195,7 @@ end end @reference_test "PolarAxis limits" begin - f = Figure(resolution = (800, 600)) + f = Figure(size = (800, 600)) for (i, theta_0) in enumerate((0, -pi/6, pi/2)) for (j, thetalims) in enumerate(((0, 2pi), (-pi/2, pi/2), (0, pi/12))) po = PolarAxis(f[i, j], theta_0 = theta_0, thetalimits = thetalims, rlimits = (1 + 2(j-1), 7)) @@ -212,7 +212,7 @@ end end @reference_test "Axis3 axis reversal" begin - f = Figure(resolution = (1000, 1000)) + f = Figure(size = (1000, 1000)) revstr(dir, rev) = rev ? "$dir rev" : "" for (i, (x, y, z)) in enumerate(Iterators.product(fill((false, true), 3)...)) Axis3(f[fldmod1(i, 3)...], title = "$(revstr("x", x)) $(revstr("y", y)) $(revstr("z", z))", xreversed = x, yreversed = y, zreversed = z) @@ -222,7 +222,7 @@ end end @reference_test "Colorbar for recipes" begin - fig, ax, pl = barplot(1:3; color=1:3, colormap=Makie.Categorical(:viridis), figure=(;resolution=(800, 800))) + fig, ax, pl = barplot(1:3; color=1:3, colormap=Makie.Categorical(:viridis), figure=(;size=(800, 800))) Colorbar(fig[1, 2], pl; size=100) x = LinRange(-1, 1, 20) y = LinRange(-1, 1, 20) @@ -273,4 +273,4 @@ end hidedecorations!(ax) axislegend(ax) fig -end \ No newline at end of file +end diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index dbc411ac2d0..7e6083f7c9c 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -1,6 +1,6 @@ @reference_test "lines and linestyles" begin # For now disabled until we fix GLMakie linestyle - s = Scene(resolution = (800, 800), camera = campixel!) + s = Scene(size = (800, 800), camera = campixel!) scalar = 30 points = Point2f[(1, 1), (1, 2), (2, 3), (2, 1)] linestyles = [ @@ -13,6 +13,7 @@ scalar .* (points .+ Point2f(linewidth*2, i * 3.25)), linewidth = linewidth, linestyle = linestyle, + color=:black ) end end @@ -20,7 +21,7 @@ end @reference_test "lines with gaps" begin - s = Scene(resolution = (800, 800), camera = campixel!) + s = Scene(size = (800, 800), camera = campixel!) points = [ Point2f[(1, 0), (2, 0.5), (NaN, NaN), (4, 0.5), (5, 0)], Point2f[(NaN, NaN), (2, 0.5), (3, 0), (4, 0.5), (5, 0)], @@ -36,7 +37,7 @@ end end @reference_test "scatters" begin - s = Scene(resolution = (800, 800), camera = campixel!) + s = Scene(size = (800, 800), camera = campixel!) markersizes = 0:2:30 markers = [:circle, :rect, :cross, :utriangle, :dtriangle, @@ -49,6 +50,7 @@ end Point2f(i, j) .* 45, marker = m, markersize = ms, + color=:black ) end end @@ -56,7 +58,7 @@ end end @reference_test "scatter rotations" begin - s = Scene(resolution = (800, 800), camera = campixel!) + s = Scene(size = (800, 800), camera = campixel!) rotations = range(0, 2pi, length = 15) markers = [:circle, :rect, :cross, :utriangle, :dtriangle, @@ -71,6 +73,7 @@ end marker = m, markersize = 30, rotations = rot, + color=:black ) scatter!(s, p, color = :red, markersize = 6) end @@ -79,7 +82,7 @@ end end @reference_test "scatter with stroke" begin - s = Scene(resolution = (350, 700), camera = campixel!) + s = Scene(size = (350, 700), camera = campixel!) # half stroke, half glow strokes = range(1, 4, length=7) @@ -112,7 +115,7 @@ end end @reference_test "scatter with glow" begin - s = Scene(resolution = (350, 700), camera = campixel!) + s = Scene(size = (350, 700), camera = campixel!) # half stroke, half glow glows = range(4, 1, length=7) @@ -148,7 +151,7 @@ end @reference_test "scatter image markers" begin pixel_types = [ RGBA, RGBAf, RGBA{Float16}, ARGB, ARGB{Float16}, RGB, RGBf, RGB{Float16} ] rotations = [ 2pi/3 * (i-1) for i = 1:length(pixel_types) ] - s = Scene(resolution = (100+100*length(pixel_types), 400), camera = campixel!) + s = Scene(size = (100+100*length(pixel_types), 400), camera = campixel!) filename = Makie.assetpath("icon_transparent.png") marker_image = load(filename) for (i, (rot, pxtype)) in enumerate(zip(rotations, pixel_types)) @@ -166,7 +169,7 @@ end @reference_test "basic polygon shapes" begin - s = Scene(resolution = (800, 800), camera = campixel!) + s = Scene(size = (800, 800), camera = campixel!) scalefactor = 70 Pol = Makie.GeometryBasics.Polygon polys = [ @@ -219,7 +222,7 @@ end @reference_test "BezierPath markers" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) ax = Axis(f[1, 1]) markers = [ @@ -255,7 +258,7 @@ end # bb = Makie.bbox(Makie.DEFAULT_MARKER_MAP[marker]) # w, h = widths(bb) # ox, oy = origin(bb) - # xy = map(pv -> Makie.project(pv, Vec2f(widths(pixelarea(scene)[])), Point2f(5, i)), scene.camera.projectionview) + # xy = map(pv -> Makie.project(pv, Vec2f(widths(viewport(scene)[])), Point2f(5, i)), scene.camera.projectionview) # bb = map(xy -> Rect2f(xy .+ 30 * Vec2f(ox, oy), 30 * Vec2f(w, h)), xy) # lines!(bb, linewidth = 1, color = :orange, space = :pixel, linestyle = :dash) # end @@ -265,7 +268,7 @@ end end @reference_test "BezierPath marker stroke" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) ax = Axis(f[1, 1]) # Same as above @@ -287,7 +290,7 @@ end @reference_test "complex_bezier_markers" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) ax = Axis(f[1, 1]) arrow = BezierPath([ @@ -401,7 +404,7 @@ end function draw_marker_test!(scene, marker, center; markersize=300) # scatter!(scene, center, distancefield=matr, uv_offset_width=Vec4f(0, 0, 1, 1), markersize=600) - scatter!(scene, center, marker=marker, markersize=markersize, markerspace=:pixel) + scatter!(scene, center, color=:black, marker=marker, markersize=markersize, markerspace=:pixel) font = Makie.defaultfont() charextent = Makie.FreeTypeAbstraction.get_extent(font, marker) @@ -422,7 +425,7 @@ function draw_marker_test!(scene, marker, center; markersize=300) end @reference_test "marke glyph alignment" begin - scene = Scene(resolution=(1200, 1200)) + scene = Scene(size=(1200, 1200)) campixel!(scene) # marker is in front, so it should not be smaller than the background rectangle plot_row!(scene, 0, false) @@ -446,8 +449,22 @@ end scene end +@reference_test "Surface with NaN points" begin + # prepare surface data + zs = [x^2 + y^2 for x in range(-2, 0, length=10), y in range(-2, 0, length=10)] + ns = copy(zs) + ns[4, 3:6] .= NaN + # plot surface + f, a, p = surface(1..10, 1..10, ns, colormap = [:lightblue, :lightblue]) + # plot a wireframe so we can see what's going on, and in which cells. + m = Makie.surface2mesh(to_value.(p.converted)...) + scatter!(a, m.position, color = isnan.(m.normals), depth_shift = -1f-3) + wireframe!(a, m, depth_shift = -1f-3, color = :black) + f +end + @reference_test "barplot with TeX-ed labels" begin - fig = Figure(resolution = (800, 800)) + fig = Figure(size = (800, 800)) lab1 = L"\int f(x) dx" lab2 = lab1 # lab2 = L"\frac{a}{b} - \sqrt{b}" # this will not work until #2667 is fixed diff --git a/ReferenceTests/src/tests/refimages.jl b/ReferenceTests/src/tests/refimages.jl index 73ce26f4a42..f66d70b4e30 100644 --- a/ReferenceTests/src/tests/refimages.jl +++ b/ReferenceTests/src/tests/refimages.jl @@ -13,6 +13,9 @@ using ReferenceTests.Colors: RGB, N0f8 using ReferenceTests.DelaunayTriangulation using Makie: Record, volume +@testset "specapi" begin + include("specapi.jl") +end @testset "primitives" begin include("primitives.jl") end diff --git a/ReferenceTests/src/tests/short_tests.jl b/ReferenceTests/src/tests/short_tests.jl index c939fc60626..b28d814a8ee 100644 --- a/ReferenceTests/src/tests/short_tests.jl +++ b/ReferenceTests/src/tests/short_tests.jl @@ -132,7 +132,7 @@ end highclip = :red, lowclip = :black, nan_color = (:green, 0.5), - shading = false, + shading = NoShading, ) surface!( Axis(fig[2, 2]), @@ -141,7 +141,7 @@ end highclip = :red, lowclip = :black, nan_color = (:green, 0.5), - shading = false, + shading = NoShading, ) fig end @@ -158,7 +158,7 @@ end @reference_test "lines linesegments width test" begin res = 200 - s = Scene(camera=campixel!, resolution=(res, res)) + s = Scene(camera=campixel!, size=(res, res)) half = res / 2 linewidth = 10 xstart = half - (half/2) diff --git a/ReferenceTests/src/tests/specapi.jl b/ReferenceTests/src/tests/specapi.jl new file mode 100644 index 00000000000..f12a39585ee --- /dev/null +++ b/ReferenceTests/src/tests/specapi.jl @@ -0,0 +1,118 @@ +import Makie.SpecApi as S + +function synchronize() + # This is very unfortunate, but deletion and updates + # are async in WGLMakie and there is no way for use to synchronize on them YET + if nameof(Makie.CURRENT_BACKEND[]) == :WGLMakie + sleep(2) + end +end + +function sync_step!(stepper) + synchronize() + Makie.step!(stepper) +end + +@reference_test "FigureSpec" begin + f, _, pl = plot(S.GridLayout()) + st = Makie.Stepper(f) + sync_step!(st) + obs = pl[1] + obs[] = S.GridLayout([S.Axis(; plots=[S.Lines(1:4; color=:black, linewidth=5), S.Scatter(1:4; markersize=20)]) + S.Axis3(; plots=[S.Scatter(Rect3f(Vec3f(0), Vec3f(1)); color=:red, markersize=50)])]) + sync_step!(st) + obs[] = begin + ax = S.Axis(; plots=[S.Scatter(1:4)]) + ax2 = S.Axis3(; title="Title 0", plots=[S.Scatter(1:4; color=1:4, markersize=20)]) + c = S.Colorbar(; limits=(0, 1), colormap=:heat) + S.GridLayout([ax ax2 c]) + end + sync_step!(st) + + obs[] = begin + p1 = S.Scatter(1:4; markersize=50) + ax = S.Axis(; plots=[p1], title="Title 1") + p2 = S.Scatter(2:4; color=1:3, markersize=30) + ax2 = S.Axis3(; plots=[p2]) + c = S.Colorbar(; limits=(2, 10), colormap=:viridis, width=50) + S.GridLayout([ax ax2 c]) + end + sync_step!(st) + ax1 = S.Axis(; plots=[S.Scatter(1:4; markersize=20), S.Lines(1:4; color=:darkred, linewidth=6)]) + ax2 = S.Axis3(; plots=[S.Scatter(Rect3f(Vec3f(0), Vec3f(1)); color=(:red, 0.5), markersize=30)]) + obs[] = S.GridLayout([ax1 ax2]) + sync_step!(st) + + elem_1 = [LineElement(; color=:red, linestyle=nothing), + MarkerElement(; color=:blue, marker='x', markersize=15, + strokecolor=:black)] + + elem_2 = [PolyElement(; color=:red, strokecolor=:blue, strokewidth=1), + LineElement(; color=:black, linestyle=:dash)] + + elem_3 = LineElement(; color=:green, linestyle=nothing, + points=Point2f[(0, 0), (0, 1), (1, 0), (1, 1)]) + + obs[] = begin + S.GridLayout(S.Legend([elem_1, elem_2, elem_3], ["elem 1", "elem 2", "elem 3"], "Legend Title")) + end + sync_step!(st) + + obs[] = begin + l = S.Legend([elem_1, elem_2], ["elem 1", "elem 2"], "New Title") + S.GridLayout(l) + end + sync_step!(st) + + obs[] = S.GridLayout() + sync_step!(st) + + st +end + +struct PlotGrid + nplots::Tuple{Int,Int} +end + +function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::PlotGrid) + plots = [S.Lines(1:4; linewidth=5, color=Cycled(1)), + S.Lines(2:5; linewidth=7, color=Cycled(2))] + axes = [S.Axis(; plots=plots) for i in 1:obj.nplots[1], j in 1:obj.nplots[2]] + return S.GridLayout(axes) +end + +struct LineScatter + show_lines::Bool + show_scatter::Bool +end +function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::LineScatter, data...) + plots = PlotSpec[] + if obj.show_lines + push!(plots, S.Lines(data...; linewidth=5)) + end + if obj.show_scatter + push!(plots, S.Scatter(data...; markersize=20)) + end + return plots +end + +@reference_test "SpecApi in convert_arguments" begin + f = Figure() + p1 = plot(f[1, 1], PlotGrid((1, 1))) + f + ax, p2 = plot(f[1, 2], LineScatter(true, true), 1:4) + st = Makie.Stepper(f) + sync_step!(st) + p1[1] = PlotGrid((2, 2)) + p2[1] = LineScatter(false, true) + sync_step!(st) + + p1[1] = PlotGrid((3, 3)) + p2[1] = LineScatter(true, false) + sync_step!(st) + + p1[1] = PlotGrid((2, 1)) + p2[1] = LineScatter(true, true) + sync_step!(st) + st +end diff --git a/ReferenceTests/src/tests/text.jl b/ReferenceTests/src/tests/text.jl index e516fae1a8f..04e4c30626a 100644 --- a/ReferenceTests/src/tests/text.jl +++ b/ReferenceTests/src/tests/text.jl @@ -1,5 +1,5 @@ @reference_test "heatmap_with_labels" begin - fig = Figure(resolution = (600, 600)) + fig = Figure(size = (600, 600)) ax = fig[1, 1] = Axis(fig) values = RNG.rand(10, 10) @@ -28,7 +28,7 @@ end end @reference_test "single_strings_single_positions" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (800, 800)) points = [Point(x, y) .* 200 for x in 1:3 for y in 1:3] scatter!(scene, points, marker = :circle, markersize = 10px) @@ -51,7 +51,7 @@ end @reference_test "multi_strings_multi_positions" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (800, 800)) angles = (-pi/6, 0.0, pi/6) points = [Point(x, y) .* 200 for x in 1:3 for y in 1:3 for angle in angles] @@ -66,8 +66,7 @@ end for valign in (:top, :center, :bottom) for rotation in angles] - scatter!(scene, points, marker = :circle, markersize = 10px) - + scatter!(scene, points, marker = :circle, markersize = 10px, color=:black) text!(scene, points, text = strings, align = aligns, rotation = rotations, color = [(:black, alpha) for alpha in LinRange(0.3, 0.7, length(points))]) @@ -76,10 +75,10 @@ end end @reference_test "single_strings_single_positions_justification" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (800, 800)) points = [Point(x, y) .* 200 for x in 1:3 for y in 1:3] - scatter!(scene, points, marker = :circle, markersize = 10px) + scatter!(scene, points, marker = :circle, markersize = 10px, color=:black) symbols = (:left, :center, :right) @@ -109,7 +108,7 @@ end end @reference_test "multi_boundingboxes" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (800, 800)) t1 = text!(scene, fill("makie", 4), @@ -137,7 +136,7 @@ end end @reference_test "single_boundingboxes" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (800, 800)) for a in pi/4:pi/2:7pi/4 @@ -176,12 +175,13 @@ end color = [cgrad(:viridis)[x] for x in LinRange(0, 1, 7)], align = (:left, :baseline), fontsize = 1, - markerspace = :data + markerspace = :data, + axis=(; type=LScene) ) end @reference_test "empty_lines" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (800, 800)) t1 = text!(scene, "Line1\nLine 2\n\nLine4", position = (200, 400), align = (:center, :center), markerspace = :data) @@ -212,7 +212,7 @@ end @reference_test "Text offset" begin - f = Figure(resolution = (1000, 1000)) + f = Figure(size = (1000, 1000)) barplot(f[1, 1], 3:5) text!(1, 3, text = "bar 1", offset = (0, 10), align = (:center, :baseline)) text!([(2, 4), (3, 5)], text = ["bar 2", "bar 3"], @@ -283,7 +283,7 @@ end position = Point2f(50, 50), rotation = 0.0, markerspace = :data) - wireframe!(s, boundingbox(t)) + wireframe!(s, boundingbox(t), color=:black) s end @@ -349,7 +349,7 @@ end @reference_test "Word Wrapping" begin lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - fig = Figure(resolution=(600, 500)) + fig = Figure(size=(600, 500)) ax = Axis(fig[1, 1]) text!(ax, 0, 0, text = latexstring(L"$1$ " * lorem_ipsum), word_wrap_width=250, fontsize = 12, align = (:left, :bottom), justification = :left, color = :black) text!(ax, 0, 0, text = lorem_ipsum, word_wrap_width=250, fontsize = 12, align = (:left, :top), justification = :right, color = :black) @@ -368,4 +368,3 @@ end ax.zlabel[] = L"\sum_{n=1}^{\infty} 2^{-n} = 1" fig end - diff --git a/ReferenceTests/src/tests/updating.jl b/ReferenceTests/src/tests/updating.jl index 27f21af75cd..d19c97647fd 100644 --- a/ReferenceTests/src/tests/updating.jl +++ b/ReferenceTests/src/tests/updating.jl @@ -43,7 +43,7 @@ end function generate_plot(N = 3) points = Observable(Point2f[]) color = Observable(RGBAf[]) - fig, ax, pl = scatter(points, color=color, markersize=1.0, marker=Circle, markerspace=:data, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(resolution=(800, 800),)) + fig, ax, pl = scatter(points, color=color, markersize=1.0, marker=Circle, markerspace=:data, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(size=(800, 800),)) function update_func(ij) push!(points.val, Point2f(Tuple(ij))) push!(color.val, RGBAf((Tuple(ij)./N)..., 0, 1)) @@ -121,10 +121,14 @@ end obs = Observable(1:5) f, ax, pl = scatter(obs; markersize=150) s = display(f) - @test length(obs.listeners) == 1 + # So, for GLMakie it will be 2, since we register an additional listener for + # State changes for the on demand renderloop + @test length(obs.listeners) in (1, 2) delete!(ax, pl) @test length(obs.listeners) == 0 - sleep(1.0) + # ugh, hard to synchronize this with WGLMakie, so, we need to sleep for now to make sure the change makes it to the browser + # TODO, add synchronization primitive? + sleep(1) f end diff --git a/WGLMakie/Project.toml b/WGLMakie/Project.toml index 92bd393c8fc..4da6b00f51d 100644 --- a/WGLMakie/Project.toml +++ b/WGLMakie/Project.toml @@ -1,7 +1,7 @@ name = "WGLMakie" uuid = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" authors = ["SimonDanisch "] -version = "0.8.16" +version = "0.9.0" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" @@ -25,8 +25,8 @@ FileIO = "1.1" FreeTypeAbstraction = "0.10" GeometryBasics = "0.4.1" Hyperscript = "0.0.3, 0.0.4" -JSServe = "2.2" -Makie = "=0.19.12" +JSServe = "v2.3" +Makie = "=0.20.0" Observables = "0.5.1" PNGFiles = "0.3, 0.4" PrecompileTools = "1.0" diff --git a/WGLMakie/assets/line_segments.frag b/WGLMakie/assets/line_segments.frag deleted file mode 100644 index 384921152a2..00000000000 --- a/WGLMakie/assets/line_segments.frag +++ /dev/null @@ -1,26 +0,0 @@ - -in vec4 frag_color; - -flat in uint frag_instance_id; -vec4 pack_int(uint id, uint index) { - vec4 unpack; - unpack.x = float((id & uint(0xff00)) >> 8) / 255.0; - unpack.y = float((id & uint(0x00ff)) >> 0) / 255.0; - unpack.z = float((index & uint(0xff00)) >> 8) / 255.0; - unpack.w = float((index & uint(0x00ff)) >> 0) / 255.0; - return unpack; -} - -void main() { - if (picking) { - if (frag_color.a > 0.1) { - fragment_color = pack_int(object_id, frag_instance_id); - } - return; - } - - if (frag_color.a <= 0.0){ - discard; - } - fragment_color = frag_color; -} diff --git a/WGLMakie/assets/line_segments.vert b/WGLMakie/assets/line_segments.vert deleted file mode 100644 index 9f088ad90c2..00000000000 --- a/WGLMakie/assets/line_segments.vert +++ /dev/null @@ -1,50 +0,0 @@ -uniform mat4 projection; -uniform mat4 view; - -vec2 screen_space(vec4 position) -{ - return vec2(position.xy / position.w) * get_resolution(); -} -vec3 tovec3(vec2 v){return vec3(v, 0.0);} -vec3 tovec3(vec3 v){return v;} - -vec4 tovec4(vec3 v){return vec4(v, 1.0);} -vec4 tovec4(vec4 v){return v;} - -out vec4 frag_color; - -flat out uint frag_instance_id; - -void main() -{ - mat4 pvm = projection * view * get_model(); - vec4 point1_clip = pvm * vec4(tovec3(get_segment_start()), 1); - vec4 point2_clip = pvm * vec4(tovec3(get_segment_end()), 1); - vec2 point1_screen = screen_space(point1_clip); - vec2 point2_screen = screen_space(point2_clip); - vec2 dir = normalize(point2_screen - point1_screen); - vec2 normal = vec2(-dir.y, dir.x); - vec4 anchor; - float thickness; - - if(position.x == 0.0){ - anchor = point1_clip; - frag_color = tovec4(get_color_start()); - thickness = get_linewidth_start(); - }else{ - anchor = point2_clip; - frag_color = tovec4(get_color_end()); - thickness = get_linewidth_end(); - } - frag_color.a = frag_color.a * min(1.0, thickness * 2.0); - - normal *= (thickness / get_resolution()) * anchor.w; - // quadpos y (position.y) gives us the direction to expand the line - vec4 offset = vec4(normal * position.y, 0.0, 0.0); - // start, or end of quad, need to use current or next point as anchor - gl_Position = anchor + offset; - gl_Position.z += gl_Position.w * get_depth_shift(); - - frag_instance_id = uint(gl_InstanceID); - -} diff --git a/WGLMakie/assets/lines.frag b/WGLMakie/assets/lines.frag new file mode 100644 index 00000000000..b146e66af6c --- /dev/null +++ b/WGLMakie/assets/lines.frag @@ -0,0 +1,47 @@ +precision mediump int; +precision mediump float; +precision mediump sampler2D; +precision mediump sampler3D; + +flat in vec2 f_uv_minmax; +in vec2 f_uv; +in vec4 f_color; +in float f_thickness; + +uniform float pattern_length; + +out vec4 fragment_color; + +// Half width of antialiasing smoothstep +#define ANTIALIAS_RADIUS 0.8 + +float aastep(float threshold1, float dist) { + return smoothstep(threshold1 - ANTIALIAS_RADIUS, threshold1 + ANTIALIAS_RADIUS, dist); +} + +float aastep(float threshold1, float threshold2, float dist) { + // We use 2x pixel space in the geometry shaders which passes through + // in uv.y, so we need to treat it here by using 2 * ANTIALIAS_RADIUS + float AA = 2.0f * ANTIALIAS_RADIUS; + return smoothstep(threshold1 - AA, threshold1 + AA, dist) - + smoothstep(threshold2 - AA, threshold2 + AA, dist); +} + +float aastep_scaled(float threshold1, float threshold2, float dist) { + float AA = ANTIALIAS_RADIUS / pattern_length; + return smoothstep(threshold1 - AA, threshold1 + AA, dist) - + smoothstep(threshold2 - AA, threshold2 + AA, dist); +} + +void main() { + vec4 color = vec4(f_color.rgb, 0.0f); + vec2 xy = f_uv; + + float alpha = aastep(0.0f, xy.x); + float alpha2 = aastep(-f_thickness, f_thickness, xy.y); + float alpha3 = aastep_scaled(f_uv_minmax.x, f_uv_minmax.y, f_uv.x); + + color = vec4(f_color.rgb, f_color.a * alpha * alpha2 * alpha3); + + fragment_color = color; +} diff --git a/WGLMakie/assets/lines.vert b/WGLMakie/assets/lines.vert new file mode 100644 index 00000000000..bb22d732b75 --- /dev/null +++ b/WGLMakie/assets/lines.vert @@ -0,0 +1,82 @@ +in float position; +in vec2 linepoint_prev; +in vec2 linepoint_start; +in vec2 linepoint_end; +in vec2 linepoint_next; +in float linewidth_prev; +in float linewidth_start; +in float linewidth_end; +in float linewidth_next; + +uniform vec4 is_valid; +uniform vec4 color_end; +uniform vec4 color_start; +uniform mat4 model; +uniform mat4 projectionview; +uniform vec2 resolution; + +out vec2 f_uv; +out vec4 f_color; +out float f_thickness; + +vec3 screen_space(vec3 point) { + vec4 vertex = projectionview * model * vec4(point, 1); + return vec3(vertex.xy * resolution, vertex.z) / vertex.w; +} + +vec3 screen_space(vec2 point) { + return screen_space(vec3(point, 0)); +} + + +void emit_vertex(vec3 position, vec2 uv, bool is_start) { + + f_uv = uv; + + f_color = is_start ? color_start : color_end; + + gl_Position = vec4((position.xy / resolution), position.z, 1.0); + // linewidth scaling may shrink the effective linewidth + f_thickness = is_start ? linewidth_start : linewidth_end; +} + +void main() { + vec3 p1 = screen_space(linepoint_start); + vec3 p2 = screen_space(linepoint_end); + vec2 dir = p1.xy - p2.xy; + dir = normalize(dir); + vec2 line_normal = vec2(dir.y, -dir.x); + vec2 line_offset = line_normal * (linewidth_start / 2.0); + + // triangle 1 + vec3 v0 = vec3(p1.xy - line_offset, p1.z); + if (position == 0.0) { + emit_vertex(v0, vec2(0.0, 0.0), true); + return; + } + vec3 v2 = vec3(p2.xy - line_offset, p2.z); + if (position == 1.0) { + emit_vertex(v2, vec2(0.0, 0.0), false); + return; + } + vec3 v1 = vec3(p1.xy + line_offset, p1.z); + if (position == 2.0) { + emit_vertex(v1, vec2(0.0, 0.0), true); + return; + } + + // triangle 2 + if (position == 3.0) { + emit_vertex(v2, vec2(0.0, 0.0), false); + return; + } + vec3 v3 = vec3(p2.xy + line_offset, p2.z); + if (position == 4.0) { + emit_vertex(v3, vec2(0.0, 0.0), false); + return; + } + if (position == 5.0) { + emit_vertex(v1, vec2(0.0, 0.0), true); + return; + } +} diff --git a/WGLMakie/assets/mesh.frag b/WGLMakie/assets/mesh.frag index 1355d41e6b6..efe137428e3 100644 --- a/WGLMakie/assets/mesh.frag +++ b/WGLMakie/assets/mesh.frag @@ -4,19 +4,31 @@ flat in int sample_frag_color; in vec3 o_normal; in vec3 o_camdir; -in vec3 o_lightdir; + +// Smoothes out edge around 0 light intensity, see GLMakie +float smooth_zero_max(float x) { + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + float pow8 = x + shift; + pow8 = pow8 * pow8; pow8 = pow8 * pow8; pow8 = pow8 * pow8; + return x < yswap ? c * pow8 : x; +} vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0); + float backlight = get_backlight(); + float diff_coeff = smooth_zero_max(dot(L, -N)) + + backlight * smooth_zero_max(dot(L, N)); // specular coefficient vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0), get_shininess()); + float spec_coeff = pow(max(dot(H, -N), 0.0), get_shininess()) + + backlight * pow(max(dot(H, N), 0.0), get_shininess()); if (diff_coeff <= 0.0) spec_coeff = 0.0; + // final lighting model - return vec3( + return get_light_color() * vec3( get_diffuse() * diff_coeff * color + get_specular() * spec_coeff ); @@ -100,11 +112,10 @@ void main() { vec3 shaded_color = real_color.rgb; if(get_shading()){ - vec3 L = normalize(o_lightdir); + vec3 L = get_light_direction(); vec3 N = normalize(o_normal); - vec3 light1 = blinnphong(N, o_camdir, L, real_color.rgb); - vec3 light2 = blinnphong(N, o_camdir, -L, real_color.rgb); - shaded_color = get_ambient() * real_color.rgb + light1 + get_backlight() * light2; + vec3 light = blinnphong(N, normalize(o_camdir), L, real_color.rgb); + shaded_color = get_ambient() * real_color.rgb + light; } if (picking) { diff --git a/WGLMakie/assets/mesh.vert b/WGLMakie/assets/mesh.vert index 3b354497fd7..14341fbe452 100644 --- a/WGLMakie/assets/mesh.vert +++ b/WGLMakie/assets/mesh.vert @@ -1,12 +1,12 @@ out vec2 frag_uv; out vec3 o_normal; out vec3 o_camdir; -out vec3 o_lightdir; out vec4 frag_color; uniform mat4 projection; uniform mat4 view; +uniform vec3 eyeposition; vec3 tovec3(vec2 v){return vec3(v, 0.0);} vec3 tovec3(vec3 v){return v;} @@ -61,22 +61,15 @@ vec4 vertex_color(float value, vec2 colorrange, sampler2D colormap){ } } -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition) +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection) { // normal in world space o_normal = get_normalmatrix() * normal; - // position in view space (as seen from camera) - vec4 view_pos = view * position_world; // position in clip space (w/ depth) - gl_Position = projection * view_pos; + gl_Position = projection * view * position_world; // TODO consider using projectionview directly gl_Position.z += gl_Position.w * get_depth_shift(); - // direction to light - o_lightdir = normalize(view*vec4(lightposition, 1.0) - view_pos).xyz; // direction to camera - // This is equivalent to - // normalize(view*vec4(eyeposition, 1.0) - view_pos).xyz - // (by definition `view * eyeposition = 0`) - o_camdir = normalize(-view_pos).xyz; + o_camdir = position_world.xyz / position_world.w - eyeposition; } flat out uint frag_instance_id; @@ -90,7 +83,7 @@ void main(){ } vec4 position_world = model * vec4(vertex_position, 1); - render(position_world, get_normals(), view, projection, get_lightposition()); + render(position_world, get_normals(), view, projection); frag_uv = get_uv(); frag_uv = vec2(1.0 - frag_uv.y, frag_uv.x); frag_color = vertex_color(get_color(), get_colorrange(), colormap); diff --git a/WGLMakie/assets/particles.frag b/WGLMakie/assets/particles.frag index 5615fb356da..262a1fd9538 100644 --- a/WGLMakie/assets/particles.frag +++ b/WGLMakie/assets/particles.frag @@ -1,23 +1,34 @@ in vec4 frag_color; in vec3 frag_normal; in vec3 frag_position; -in vec3 frag_lightdir; +in vec3 o_camdir; + +// Smoothes out edge around 0 light intensity, see GLMakie +float smooth_zero_max(float x) { + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + float pow8 = x + shift; + pow8 = pow8 * pow8; pow8 = pow8 * pow8; pow8 = pow8 * pow8; + return x < yswap ? c * pow8 : x; +} vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0); + float backlight = get_backlight(); + float diff_coeff = smooth_zero_max(dot(L, -N)) + + backlight * smooth_zero_max(dot(L, N)); // specular coefficient - vec3 H = normalize(L+V); + vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0), 8.0); + float spec_coeff = pow(max(dot(H, -N), 0.0), get_shininess()) + + backlight * pow(max(dot(H, N), 0.0), get_shininess()); if (diff_coeff <= 0.0) spec_coeff = 0.0; // final lighting model - return vec3( - vec3(0.1) * vec3(0.3) + - vec3(0.9) * color * diff_coeff + - vec3(0.3) * spec_coeff + return get_light_color() * vec3( + get_diffuse() * diff_coeff * color + + get_specular() * spec_coeff ); } @@ -32,13 +43,12 @@ vec4 pack_int(uint id, uint index) { } void main() { - vec3 L, N, light1, light2, color; + vec3 L, N, light, color; if (get_shading()) { - L = normalize(frag_lightdir); + L = get_light_direction(); 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; + light = blinnphong(N, normalize(o_camdir), L, frag_color.rgb); + color = get_ambient() * frag_color.rgb + light; } else { color = frag_color.rgb; } diff --git a/WGLMakie/assets/particles.vert b/WGLMakie/assets/particles.vert index f2785d2aed4..e9bd0a356c3 100644 --- a/WGLMakie/assets/particles.vert +++ b/WGLMakie/assets/particles.vert @@ -2,13 +2,12 @@ precision mediump float; uniform mat4 projection; uniform mat4 view; +uniform vec3 eyeposition; out vec3 frag_normal; out vec3 frag_position; - out vec4 frag_color; -out vec3 frag_lightdir; - +out vec3 o_camdir; vec3 qmul(vec4 q, vec3 v){ return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v); @@ -31,16 +30,14 @@ 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() * to_vec3(get_position()); - vec3 lightpos = vec3(20,20,20); vec3 N = get_normals(); rotate(get_rotations(), vertex_position, N); vertex_position = to_vec3(get_offset()) + vertex_position; vec4 position_world = model * vec4(vertex_position, 1); frag_normal = N; - frag_lightdir = normalize(lightpos - position_world.xyz); frag_color = to_vec4(get_color()); // direction to camera - frag_position = -position_world.xyz; + o_camdir = position_world.xyz / position_world.w - eyeposition; // screen space coordinates of the position gl_Position = projection * view * position_world; gl_Position.z += gl_Position.w * get_depth_shift(); diff --git a/WGLMakie/assets/sprites.vert b/WGLMakie/assets/sprites.vert index 42a7c4a9cc6..35f8eaedd86 100644 --- a/WGLMakie/assets/sprites.vert +++ b/WGLMakie/assets/sprites.vert @@ -61,15 +61,19 @@ void main(){ vec2 sprite_bbox_centre = get_quad_offset() + bbox_signed_radius; mat4 pview = projection * view; - // Compute transform for the offset vectors from the central point mat4 trans = get_transform_marker() ? model : mat4(1.0); - trans = (get_billboard() ? projection : pview) * qmat(get_rotations()) * trans; // Compute centre of billboard in clipping coordinates - vec4 sprite_center = trans * vec4(sprite_bbox_centre, 0, 0); + // Always transform text/scatter position argument vec4 data_point = get_preprojection() * model * vec4(tovec3(get_pos()), 1); - data_point = vec4(data_point.xyz / data_point.w + mat3(model) * tovec3(get_marker_offset()), 1); + // maybe transform marker_offset + glyph offsets + data_point = vec4(data_point.xyz / data_point.w + mat3(trans) * tovec3(get_marker_offset()), 1); data_point = pview * data_point; + + // Compute transform for the offset vectors from the central point + trans = (get_billboard() ? projection : pview) * qmat(get_rotations()) * trans; + vec4 sprite_center = trans * vec4(sprite_bbox_centre, 0, 0); + vec4 vclip = data_point + sprite_center; // Extra buffering is required around sprites which are antialiased so that @@ -88,7 +92,7 @@ void main(){ 0.0, 0.0, 1.0/vclip.w, 0.0, -vclip.xyz/(vclip.w*vclip.w), 0.0 ); - mat2 dxyv_dxys = diagm(0.5 * get_resolution()) * mat2(d_ndc_d_clip*trans); + mat2 dxyv_dxys = diagm(0.5 * px_per_unit * get_resolution()) * mat2(d_ndc_d_clip*trans); // Now, our buffer size is expressed in viewport pixels but we get back to // the sprite coordinate system using the scale factor of the // transformation (for isotropic transformations). For anisotropic diff --git a/WGLMakie/assets/volume.frag b/WGLMakie/assets/volume.frag index ec37e457671..d3048f8afee 100644 --- a/WGLMakie/assets/volume.frag +++ b/WGLMakie/assets/volume.frag @@ -2,7 +2,6 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d bool _; //empty structs are not allowed }; in vec3 frag_vert; -in vec3 o_light_dir; const float max_distance = 1.3; @@ -54,16 +53,25 @@ vec3 gennormal(vec3 uvw, float d) return normalize(a-b); } +// Smoothes out edge around 0 light intensity, see GLMakie +float smooth_zero_max(float x) { + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + float pow8 = x + shift; + pow8 = pow8 * pow8; pow8 = pow8 * pow8; pow8 = pow8 * pow8; + return x < yswap ? c * pow8 : x; +} + vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0) + max(dot(L, -N), 0.0); + // TODO use backlight here too? + float diff_coeff = smooth_zero_max(dot(L, -N)) + smooth_zero_max(dot(L, N)); // specular coefficient vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0) + max(dot(H, -N), 0.0), shininess); + float spec_coeff = pow(max(dot(H, -N), 0.0) + max(dot(H, N), 0.0), shininess); // final lighting model - return vec3( - ambient * color + - diffuse * diff_coeff * color + - specular * spec_coeff + return ambient * color + get_light_color() * vec3( + get_diffuse() * diff_coeff * color + + get_specular() * spec_coeff ); } @@ -122,14 +130,14 @@ vec4 contours(vec3 front, vec3 dir) float T = 1.0; vec3 Lo = vec3(0.0); int i = 0; - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); for (i; i < num_samples; ++i) { float intensity = texture(volumedata, pos).x; vec4 density = color_lookup(intensity, colormap, colorrange); float opacity = density.a; if(opacity > 0.0){ vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); + vec3 L = get_light_direction(); vec3 opaque = blinnphong(N, camdir, L, density.rgb); Lo += (T * opacity) * opaque; T *= 1.0 - opacity; @@ -147,12 +155,12 @@ vec4 isosurface(vec3 front, vec3 dir) vec4 c = vec4(0.0); int i = 0; vec4 diffuse_color = color_lookup(isovalue, colormap, colorrange); - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); for (i; i < num_samples; ++i){ float density = texture(volumedata, pos).x; if(abs(density - isovalue) < isorange){ vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); + vec3 L = get_light_direction(); c = vec4( blinnphong(N, camdir, L, diffuse_color.rgb), diffuse_color.a diff --git a/WGLMakie/assets/volume.vert b/WGLMakie/assets/volume.vert index 42599e3e6a3..c9d00be85b8 100644 --- a/WGLMakie/assets/volume.vert +++ b/WGLMakie/assets/volume.vert @@ -1,5 +1,4 @@ out vec3 frag_vert; -out vec3 o_light_dir; uniform mat4 projection, view; @@ -7,7 +6,6 @@ void main() { frag_vert = position; vec4 world_vert = model * vec4(position, 1); - o_light_dir = vec3(modelinv * vec4(get_lightposition(), 1)); gl_Position = projection * view * world_vert; gl_Position.z += gl_Position.w * get_depth_shift(); } diff --git a/WGLMakie/src/Camera.js b/WGLMakie/src/Camera.js index beef23e51f1..1a2b02d3655 100644 --- a/WGLMakie/src/Camera.js +++ b/WGLMakie/src/Camera.js @@ -1,18 +1,20 @@ -import * as THREE from "https://cdn.esm.sh/v66/three@0.157/es2021/three.js"; - -const pixelRatio = window.devicePixelRatio || 1.0; - -export function event2scene_pixel(scene, event) { - const { canvas } = scene.screen; +import * as THREE from "./THREE.js"; +import { OrbitControls } from "./OrbitControls.js"; + +// Unitless is the scene pixel unit space +// so scene.viewport, or size(scene) +// Which isn't the same as the framebuffer pixel size due to scalefactor/px_per_unit/devicePixelRatio +export function events2unitless(screen, event) { + const { canvas, winscale, renderer } = screen; const rect = canvas.getBoundingClientRect(); - const x = (event.clientX - rect.left) * pixelRatio; - const y = (rect.height - (event.clientY - rect.top)) * pixelRatio; - return [x, y]; + const x = (event.clientX - rect.left) / winscale; + const y = (event.clientY - rect.top) / winscale; + return [x, renderer._height - y]; } export function to_world(scene, x, y) { const proj_inv = scene.wgl_camera.projectionview_inverse.value; - const [_x, _y, w, h] = scene.pixelarea.value; + const [_x, _y, w, h] = scene.viewport.value; const pix_space = new THREE.Vector4( ((x - _x) / w) * 2 - 1, ((y - _y) / h) * 2 - 1, @@ -32,30 +34,42 @@ function Identity4x4() { } function in_scene(scene, mouse_event) { - const [x, y] = event2scene_pixel(scene, mouse_event); - const [sx, sy, sw, sh] = scene.pixelarea.value; + const [x, y] = events2unitless(scene.screen, mouse_event); + const [sx, sy, sw, sh] = scene.viewport.value; return x >= sx && x < sx + sw && y >= sy && y < sy + sh; } // Taken from https://andreasrohner.at/posts/Web%20Development/JavaScript/Simple-orbital-camera-controls-for-THREE-js/ -export function attach_3d_camera(canvas, makie_camera, cam3d, scene) { +export function attach_3d_camera( + canvas, + makie_camera, + cam3d, + light_dir, + scene +) { if (cam3d === undefined) { // we just support 3d cameras atm return; } const [w, h] = makie_camera.resolution.value; const camera = new THREE.PerspectiveCamera( - cam3d.fov, + cam3d.fov.value, w / h, - cam3d.near, - cam3d.far + 0.01, + 100.0 ); - const center = new THREE.Vector3(...cam3d.lookat); - camera.up = new THREE.Vector3(...cam3d.upvector); - camera.position.set(...cam3d.eyeposition); + const center = new THREE.Vector3(...cam3d.lookat.value); + camera.up = new THREE.Vector3(...cam3d.upvector.value); + camera.position.set(...cam3d.eyeposition.value); camera.lookAt(center); - function update() { + + const use_orbit_cam = () => + !(JSServe.can_send_to_julia && JSServe.can_send_to_julia()); + const controls = new OrbitControls(camera, canvas, use_orbit_cam, (e) => + in_scene(scene, e) + ); + controls.addEventListener("change", (e) => { const view = camera.matrixWorldInverse; const projection = camera.projectionMatrix; const [width, height] = cam3d.resolution.value; @@ -63,103 +77,14 @@ export function attach_3d_camera(canvas, makie_camera, cam3d, scene) { camera.aspect = width / height; camera.updateProjectionMatrix(); camera.updateWorldMatrix(); - makie_camera.update_matrices( view.elements, projection.elements, [width, height], [x, y, z] - ); - } - cam3d.resolution.on(update); - - function addMouseHandler(domObject, drag, zoomIn, zoomOut) { - let startDragX = null; - let startDragY = null; - function mouseWheelHandler(e) { - e = window.event || e; - if (!in_scene(scene, e)) { - return; - } - const delta = Math.sign(e.deltaY); - if (delta == -1) { - zoomOut(); - } else if (delta == 1) { - zoomIn(); - } - - e.preventDefault(); - } - function mouseDownHandler(e) { - if (!in_scene(scene, e)) { - return; - } - startDragX = e.clientX; - startDragY = e.clientY; - - e.preventDefault(); - } - function mouseMoveHandler(e) { - if (!in_scene(scene, e)) { - return; - } - if (startDragX === null || startDragY === null) return; - - if (drag) drag(e.clientX - startDragX, e.clientY - startDragY); - - startDragX = e.clientX; - startDragY = e.clientY; - e.preventDefault(); - } - function mouseUpHandler(e) { - if (!in_scene(scene, e)) { - return; - } - mouseMoveHandler.call(this, e); - startDragX = null; - startDragY = null; - e.preventDefault(); - } - domObject.addEventListener("wheel", mouseWheelHandler); - domObject.addEventListener("mousedown", mouseDownHandler); - domObject.addEventListener("mousemove", mouseMoveHandler); - domObject.addEventListener("mouseup", mouseUpHandler); - } - - function drag(deltaX, deltaY) { - const radPerPixel = Math.PI / 450; - const deltaPhi = radPerPixel * deltaX; - const deltaTheta = radPerPixel * deltaY; - const pos = camera.position.sub(center); - const radius = pos.length(); - let theta = Math.acos(pos.z / radius); - let phi = Math.atan2(pos.y, pos.x); - - // Subtract deltaTheta and deltaPhi - theta = Math.min(Math.max(theta - deltaTheta, 0), Math.PI); - phi -= deltaPhi; - - // Turn back into Cartesian coordinates - pos.x = radius * Math.sin(theta) * Math.cos(phi); - pos.y = radius * Math.sin(theta) * Math.sin(phi); - pos.z = radius * Math.cos(theta); - - camera.position.add(center); - camera.lookAt(center); - update(); - } - - function zoomIn() { - camera.position.sub(center).multiplyScalar(0.9).add(center); - update(); - } - - function zoomOut() { - camera.position.sub(center).multiplyScalar(1.1).add(center); - update(); - } - - addMouseHandler(canvas, drag, zoomIn, zoomOut); + ); + makie_camera.update_light_dir(light_dir.value); + }); } function mul(a, b) { @@ -239,6 +164,12 @@ export class MakieCamera { // Lazy calculation, only if a plot type requests them // will be of the form: {[space, markerspace]: THREE.Uniform(...)} this.preprojections = {}; + + // For camera-relative light directions + // TODO: intial position wrong... + this.light_direction = new THREE.Uniform( + new THREE.Vector3(-1, -1, -1).normalize() + ); } calculate_matrices() { @@ -260,7 +191,8 @@ export class MakieCamera { // update all existing preprojection matrices Object.keys(this.preprojections).forEach((key) => { const [space, markerspace] = key.split(","); // jeez js, really just converting array keys to "elem,elem"? - this.preprojections[key].value = this.calculate_preprojection_matrix(space, markerspace); + this.preprojections[key].value = + this.calculate_preprojection_matrix(space, markerspace); }); } @@ -273,6 +205,13 @@ export class MakieCamera { return; } + update_light_dir(light_dir) { + const T = new THREE.Matrix3().setFromMatrix4(this.view.value).invert(); + const new_dir = new THREE.Vector3().fromArray(light_dir); + new_dir.applyMatrix3(T).normalize(); + this.light_direction.value = new_dir; + } + clip_to_space(space) { if (space === "data") { return this.projectionview_inverse.value; diff --git a/WGLMakie/src/Lines.js b/WGLMakie/src/Lines.js new file mode 100644 index 00000000000..8fda06ae4eb --- /dev/null +++ b/WGLMakie/src/Lines.js @@ -0,0 +1,285 @@ +import { + attributes_to_type_declaration, + uniforms_to_type_declaration, + uniform_type, + attribute_type, +} from "./Shaders.js"; + +import { deserialize_uniforms } from "./Serialization.js"; + +function filter_by_key(dict, keys, default_value = false) { + const result = {}; + keys.forEach((key) => { + const val = dict[key]; + if (val) { + result[key] = val; + } else { + result[key] = default_value; + } + }); + return result; +} + +// https://github.com/glslify/glsl-aastep +// https://wwwtyro.net/2019/11/18/instanced-lines.html +// https://github.com/mrdoob/three.js/blob/dev/examples/jsm/lines/LineMaterial.js +// https://www.khronos.org/assets/uploads/developers/presentations/Crazy_Panda_How_to_draw_lines_in_WebGL.pdf +// https://github.com/gameofbombs/pixi-candles/tree/master/src +// https://github.com/wwwtyro/instanced-lines-demos/tree/master +function linesegments_vertex_shader(uniforms, attributes) { + const attribute_decl = attributes_to_type_declaration(attributes); + const uniform_decl = uniforms_to_type_declaration(uniforms); + const color = + attribute_type(attributes.color_start) || + uniform_type(uniforms.color_start); + + return `precision mediump int; + precision highp float; + + ${attribute_decl} + ${uniform_decl} + + out vec2 f_uv; + out ${color} f_color; + + vec2 get_resolution() { + // 2 * px_per_unit doesn't make any sense, but works + // TODO, figure out what's going on! + return resolution / 2.0 * px_per_unit; + } + + vec3 screen_space(vec3 point) { + vec4 vertex = projectionview * model * vec4(point, 1); + return vec3(vertex.xy * get_resolution(), vertex.z + vertex.w * depth_shift) / vertex.w; + } + + vec3 screen_space(vec2 point) { + return screen_space(vec3(point, 0)); + } + + void main() { + vec3 p_a = screen_space(linepoint_start); + vec3 p_b = screen_space(linepoint_end); + float width = (px_per_unit * (position.x == 1.0 ? linewidth_end : linewidth_start)); + f_color = position.x == 1.0 ? color_end : color_start; + f_uv = vec2(position.x, position.y + 0.5); + + vec2 pointA = p_a.xy; + vec2 pointB = p_b.xy; + + vec2 xBasis = pointB - pointA; + vec2 yBasis = normalize(vec2(-xBasis.y, xBasis.x)); + vec2 point = pointA + xBasis * position.x + yBasis * width * position.y; + + gl_Position = vec4(point.xy / get_resolution(), position.x == 1.0 ? p_b.z : p_a.z, 1.0); + } + `; +} + +function lines_fragment_shader(uniforms, attributes) { + const color = + attribute_type(attributes.color_start) || + uniform_type(uniforms.color_start); + const color_uniforms = filter_by_key(uniforms, [ + "colorrange", + "colormap", + "nan_color", + "highclip", + "lowclip", + ]); + const uniform_decl = uniforms_to_type_declaration(color_uniforms); + + return `#extension GL_OES_standard_derivatives : enable + + precision mediump int; + precision highp float; + precision mediump sampler2D; + precision mediump sampler3D; + + in vec2 f_uv; + in ${color} f_color; + ${uniform_decl} + + out vec4 fragment_color; + + // Half width of antialiasing smoothstep + #define ANTIALIAS_RADIUS 0.7071067811865476 + + vec4 get_color_from_cmap(float value, sampler2D colormap, vec2 colorrange) { + float cmin = colorrange.x; + float cmax = colorrange.y; + if (value <= cmax && value >= cmin) { + // in value range, continue! + } else if (value < cmin) { + return lowclip; + } else if (value > cmax) { + return highclip; + } else { + // isnan CAN be broken (of course) -.- + // so if outside value range and not smaller/bigger min/max we assume NaN + return nan_color; + } + float i01 = clamp((value - cmin) / (cmax - cmin), 0.0, 1.0); + // 1/0 corresponds to the corner of the colormap, so to properly interpolate + // between the colors, we need to scale it, so that the ends are at 1 - (stepsize/2) and 0+(stepsize/2). + float stepsize = 1.0 / float(textureSize(colormap, 0)); + i01 = (1.0 - stepsize) * i01 + 0.5 * stepsize; + return texture(colormap, vec2(i01, 0.0)); + } + + vec4 get_color(float color, sampler2D colormap, vec2 colorrange) { + return get_color_from_cmap(color, colormap, colorrange); + } + + vec4 get_color(vec4 color, bool colormap, bool colorrange) { + return color; + } + vec4 get_color(vec3 color, bool colormap, bool colorrange) { + return vec4(color, 1.0); + } + + float aastep(float threshold, float value) { + float afwidth = length(vec2(dFdx(value), dFdy(value))) * ANTIALIAS_RADIUS; + return smoothstep(threshold-afwidth, threshold+afwidth, value); + } + + float aastep(float threshold1, float threshold2, float dist) { + return aastep(threshold1, dist) * aastep(threshold2, 1.0 - dist); + } + + void main(){ + float xalpha = aastep(0.0, 0.0, f_uv.x); + float yalpha = aastep(0.0, 0.0, f_uv.y); + vec4 color = get_color(f_color, colormap, colorrange); + fragment_color = vec4(color.rgb, color.a); + } + `; +} + +function create_line_material(uniforms, attributes) { + const uniforms_des = deserialize_uniforms(uniforms); + return new THREE.RawShaderMaterial({ + uniforms: uniforms_des, + glslVersion: THREE.GLSL3, + vertexShader: linesegments_vertex_shader(uniforms_des, attributes), + fragmentShader: lines_fragment_shader(uniforms_des, attributes), + transparent: true, + }); +} + +function attach_interleaved_line_buffer(attr_name, geometry, points, ndim, is_segments) { + const skip_elems = is_segments ? 2 * ndim : ndim; + const buffer = new THREE.InstancedInterleavedBuffer(points, skip_elems, 1); + geometry.setAttribute( + attr_name + "_start", + new THREE.InterleavedBufferAttribute(buffer, ndim, 0) + ); // xyz1 + geometry.setAttribute( + attr_name + "_end", + new THREE.InterleavedBufferAttribute(buffer, ndim, ndim) + ); // xyz1 + return buffer; +} + +function create_line_instance_geometry() { + const geometry = new THREE.InstancedBufferGeometry(); + const instance_positions = [ + 0, -0.5, 1, -0.5, 1, 0.5, + + 0, -0.5, 1, 0.5, 0, 0.5, + ]; + geometry.setAttribute( + "position", + new THREE.Float32BufferAttribute(instance_positions, 2) + ); + geometry.boundingSphere = new THREE.Sphere(); + // don't use intersection / culling + geometry.boundingSphere.radius = 10000000000000; + geometry.frustumCulled = false; + return geometry; +} + +function create_line_buffer(geometry, buffers, name, attr, is_segments) { + const flat_buffer = attr.value.flat; + const ndims = attr.value.type_length; + const linebuffer = attach_interleaved_line_buffer( + name, + geometry, + flat_buffer, + ndims, + is_segments + ); + buffers[name] = linebuffer; + return flat_buffer; +} + +function create_line_buffers(geometry, buffers, attributes, is_segments) { + for (let name in attributes) { + const attr = attributes[name]; + create_line_buffer(geometry, buffers, name, attr, is_segments); + } +} + +function attach_updates(mesh, buffers, attributes, is_segments) { + let geometry = mesh.geometry; + for (let name in attributes) { + const attr = attributes[name]; + attr.on((new_points) => { + let buff = buffers[name]; + const ndims = new_points.type_length; + const new_line_points = new_points.flat; + const old_count = buff.array.length; + const new_count = new_line_points.length / ndims; + if (old_count < new_line_points.length) { + mesh.geometry.dispose(); + geometry = create_line_instance_geometry(); + buff = attach_interleaved_line_buffer( + name, + geometry, + new_line_points, + ndims, + is_segments + ); + mesh.geometry = geometry; + buffers[name] = buff; + } else { + buff.set(new_line_points); + } + const ls_factor = is_segments ? 2 : 1; + const offset = is_segments ? 0 : 1; + mesh.geometry.instanceCount = Math.max(0, (new_count / ls_factor) - offset); + buff.needsUpdate = true; + mesh.needsUpdate = true; + }); + } +} + +export function _create_line(line_data, is_segments) { + const geometry = create_line_instance_geometry(); + const buffers = {}; + create_line_buffers( + geometry, + buffers, + line_data.attributes, + is_segments + ); + const material = create_line_material( + line_data.uniforms, + geometry.attributes + ); + + const mesh = new THREE.Mesh(geometry, material); + const offset = is_segments ? 0 : 1; + const new_count = geometry.attributes.linepoint_start.count; + mesh.geometry.instanceCount = Math.max(0, new_count - offset); + attach_updates(mesh, buffers, line_data.attributes, is_segments); + return mesh; +} + +export function create_line(line_data) { + return _create_line(line_data, false) +} + +export function create_linesegments(line_data) { + return _create_line(line_data, true) +} diff --git a/WGLMakie/src/OrbitControls.js b/WGLMakie/src/OrbitControls.js new file mode 100644 index 00000000000..8a39fcb2332 --- /dev/null +++ b/WGLMakie/src/OrbitControls.js @@ -0,0 +1,1249 @@ +// Taken from three.js OrbitControls.js +// https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/jsm/controls/OrbitControls.js +import { + EventDispatcher, + MOUSE, + Quaternion, + Spherical, + TOUCH, + Vector2, + Vector3, + Plane, + Ray, + MathUtils, +} from "./THREE.js"; + +// OrbitControls performs orbiting, dollying (zooming), and panning. +// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). +// +// Orbit - left mouse / touch: one-finger move +// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish +// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move + +const _changeEvent = { type: "change" }; +const _startEvent = { type: "start" }; +const _endEvent = { type: "end" }; +const _ray = new Ray(); +const _plane = new Plane(); +const TILT_LIMIT = Math.cos(70 * MathUtils.DEG2RAD); + +class OrbitControls extends EventDispatcher { + constructor(object, domElement, allow_update, is_in_scene) { + super(); + + this.object = object; + this.domElement = domElement; + this.domElement.style.touchAction = "none"; // disable touch scroll + + // Set to false to disable this control + this.enabled = true; + + // "target" sets the location of focus, where the object orbits around + this.target = new Vector3(); + + // Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effect + this.cursor = new Vector3(); + + // How far you can dolly in and out ( PerspectiveCamera only ) + this.minDistance = 0; + this.maxDistance = Infinity; + + // How far you can zoom in and out ( OrthographicCamera only ) + this.minZoom = 0; + this.maxZoom = Infinity; + + // Limit camera target within a spherical area around the cursor + this.minTargetRadius = 0; + this.maxTargetRadius = Infinity; + + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + this.minPolarAngle = 0; // radians + this.maxPolarAngle = Math.PI; // radians + + // How far you can orbit horizontally, upper and lower limits. + // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) + this.minAzimuthAngle = -Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians + + // Set to true to enable damping (inertia) + // If damping is enabled, you must call controls.update() in your animation loop + this.enableDamping = false; + this.dampingFactor = 0.05; + + // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. + // Set to false to disable zooming + this.enableZoom = true; + this.zoomSpeed = 1.0; + + // Set to false to disable rotating + this.enableRotate = true; + this.rotateSpeed = 1.0; + + // Set to false to disable panning + this.enablePan = true; + this.panSpeed = 1.0; + this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up + this.keyPanSpeed = 7.0; // pixels moved per arrow key push + this.zoomToCursor = false; + + // Set to true to automatically rotate around the target + // If auto-rotate is enabled, you must call controls.update() in your animation loop + this.autoRotate = false; + this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 + + // The four arrow keys + this.keys = { + LEFT: "ArrowLeft", + UP: "ArrowUp", + RIGHT: "ArrowRight", + BOTTOM: "ArrowDown", + }; + + // Mouse buttons + this.mouseButtons = { + LEFT: MOUSE.ROTATE, + MIDDLE: MOUSE.DOLLY, + RIGHT: MOUSE.PAN, + }; + + // Touch fingers + this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; + + // for reset + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; + + // the target DOM element for key events + this._domElementKeyEvents = null; + + // + // public methods + // + + this.getPolarAngle = function () { + return spherical.phi; + }; + + this.getAzimuthalAngle = function () { + return spherical.theta; + }; + + this.getDistance = function () { + return this.object.position.distanceTo(this.target); + }; + + this.listenToKeyEvents = function (domElement) { + domElement.addEventListener("keydown", onKeyDown); + this._domElementKeyEvents = domElement; + }; + + this.stopListenToKeyEvents = function () { + this._domElementKeyEvents.removeEventListener("keydown", onKeyDown); + this._domElementKeyEvents = null; + }; + + this.saveState = function () { + scope.target0.copy(scope.target); + scope.position0.copy(scope.object.position); + scope.zoom0 = scope.object.zoom; + }; + + this.reset = function () { + scope.target.copy(scope.target0); + scope.object.position.copy(scope.position0); + scope.object.zoom = scope.zoom0; + + scope.object.updateProjectionMatrix(); + scope.dispatchEvent(_changeEvent); + + scope.update(); + + state = STATE.NONE; + }; + + // this method is exposed, but perhaps it would be better if we can make it private... + this.update = (function () { + const offset = new Vector3(); + + // so camera.up is the orbit axis + const quat = new Quaternion().setFromUnitVectors( + object.up, + new Vector3(0, 1, 0) + ); + const quatInverse = quat.clone().invert(); + + const lastPosition = new Vector3(); + const lastQuaternion = new Quaternion(); + const lastTargetPosition = new Vector3(); + + const twoPI = 2 * Math.PI; + + return function update(deltaTime = null) { + if (!allow_update()) { + return; + } + const position = scope.object.position; + + offset.copy(position).sub(scope.target); + + // rotate offset to "y-axis-is-up" space + offset.applyQuaternion(quat); + + // angle from z-axis around y-axis + spherical.setFromVector3(offset); + + if (scope.autoRotate && state === STATE.NONE) { + rotateLeft(getAutoRotationAngle(deltaTime)); + } + + if (scope.enableDamping) { + spherical.theta += + sphericalDelta.theta * scope.dampingFactor; + spherical.phi += sphericalDelta.phi * scope.dampingFactor; + } else { + spherical.theta += sphericalDelta.theta; + spherical.phi += sphericalDelta.phi; + } + + // restrict theta to be between desired limits + + let min = scope.minAzimuthAngle; + let max = scope.maxAzimuthAngle; + + if (isFinite(min) && isFinite(max)) { + if (min < -Math.PI) min += twoPI; + else if (min > Math.PI) min -= twoPI; + + if (max < -Math.PI) max += twoPI; + else if (max > Math.PI) max -= twoPI; + + if (min <= max) { + spherical.theta = Math.max( + min, + Math.min(max, spherical.theta) + ); + } else { + spherical.theta = + spherical.theta > (min + max) / 2 + ? Math.max(min, spherical.theta) + : Math.min(max, spherical.theta); + } + } + + // restrict phi to be between desired limits + spherical.phi = Math.max( + scope.minPolarAngle, + Math.min(scope.maxPolarAngle, spherical.phi) + ); + + spherical.makeSafe(); + + // move target to panned location + + if (scope.enableDamping === true) { + scope.target.addScaledVector( + panOffset, + scope.dampingFactor + ); + } else { + scope.target.add(panOffset); + } + + // Limit the target distance from the cursor to create a sphere around the center of interest + scope.target.sub(scope.cursor); + scope.target.clampLength( + scope.minTargetRadius, + scope.maxTargetRadius + ); + scope.target.add(scope.cursor); + + // adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera + // we adjust zoom later in these cases + if ( + (scope.zoomToCursor && performCursorZoom) || + scope.object.isOrthographicCamera + ) { + spherical.radius = clampDistance(spherical.radius); + } else { + spherical.radius = clampDistance(spherical.radius * scale); + } + + offset.setFromSpherical(spherical); + + // rotate offset back to "camera-up-vector-is-up" space + offset.applyQuaternion(quatInverse); + + position.copy(scope.target).add(offset); + + scope.object.lookAt(scope.target); + + if (scope.enableDamping === true) { + sphericalDelta.theta *= 1 - scope.dampingFactor; + sphericalDelta.phi *= 1 - scope.dampingFactor; + + panOffset.multiplyScalar(1 - scope.dampingFactor); + } else { + sphericalDelta.set(0, 0, 0); + + panOffset.set(0, 0, 0); + } + + // adjust camera position + let zoomChanged = false; + if (scope.zoomToCursor && performCursorZoom) { + let newRadius = null; + if (scope.object.isPerspectiveCamera) { + // move the camera down the pointer ray + // this method avoids floating point error + const prevRadius = offset.length(); + newRadius = clampDistance(prevRadius * scale); + + const radiusDelta = prevRadius - newRadius; + scope.object.position.addScaledVector( + dollyDirection, + radiusDelta + ); + scope.object.updateMatrixWorld(); + } else if (scope.object.isOrthographicCamera) { + // adjust the ortho camera position based on zoom changes + const mouseBefore = new Vector3(mouse.x, mouse.y, 0); + mouseBefore.unproject(scope.object); + + scope.object.zoom = Math.max( + scope.minZoom, + Math.min(scope.maxZoom, scope.object.zoom / scale) + ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + const mouseAfter = new Vector3(mouse.x, mouse.y, 0); + mouseAfter.unproject(scope.object); + + scope.object.position.sub(mouseAfter).add(mouseBefore); + scope.object.updateMatrixWorld(); + + newRadius = offset.length(); + } else { + console.warn( + "WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled." + ); + scope.zoomToCursor = false; + } + + // handle the placement of the target + if (newRadius !== null) { + if (this.screenSpacePanning) { + // position the orbit target in front of the new camera position + scope.target + .set(0, 0, -1) + .transformDirection(scope.object.matrix) + .multiplyScalar(newRadius) + .add(scope.object.position); + } else { + // get the ray and translation plane to compute target + _ray.origin.copy(scope.object.position); + _ray.direction + .set(0, 0, -1) + .transformDirection(scope.object.matrix); + + // if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid + // extremely large values + if ( + Math.abs(scope.object.up.dot(_ray.direction)) < + TILT_LIMIT + ) { + object.lookAt(scope.target); + } else { + _plane.setFromNormalAndCoplanarPoint( + scope.object.up, + scope.target + ); + _ray.intersectPlane(_plane, scope.target); + } + } + } + } else if (scope.object.isOrthographicCamera) { + scope.object.zoom = Math.max( + scope.minZoom, + Math.min(scope.maxZoom, scope.object.zoom / scale) + ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + } + + scale = 1; + performCursorZoom = false; + + // update condition is: + // min(camera displacement, camera rotation in radians)^2 > EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + + if ( + zoomChanged || + lastPosition.distanceToSquared(scope.object.position) > + EPS || + 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > + EPS || + lastTargetPosition.distanceToSquared(scope.target) > 0 + ) { + scope.dispatchEvent(_changeEvent); + + lastPosition.copy(scope.object.position); + lastQuaternion.copy(scope.object.quaternion); + lastTargetPosition.copy(scope.target); + + zoomChanged = false; + + return true; + } + + return false; + }; + })(); + + this.dispose = function () { + scope.domElement.removeEventListener("contextmenu", onContextMenu); + + scope.domElement.removeEventListener("pointerdown", onPointerDown); + scope.domElement.removeEventListener("pointercancel", onPointerUp); + scope.domElement.removeEventListener("wheel", onMouseWheel); + + scope.domElement.removeEventListener("pointermove", onPointerMove); + scope.domElement.removeEventListener("pointerup", onPointerUp); + + if (scope._domElementKeyEvents !== null) { + scope._domElementKeyEvents.removeEventListener( + "keydown", + onKeyDown + ); + scope._domElementKeyEvents = null; + } + + //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? + }; + + // + // internals + // + + const scope = this; + + const STATE = { + NONE: -1, + ROTATE: 0, + DOLLY: 1, + PAN: 2, + TOUCH_ROTATE: 3, + TOUCH_PAN: 4, + TOUCH_DOLLY_PAN: 5, + TOUCH_DOLLY_ROTATE: 6, + }; + + let state = STATE.NONE; + + const EPS = 0.000001; + + // current position in spherical coordinates + const spherical = new Spherical(); + const sphericalDelta = new Spherical(); + + let scale = 1; + const panOffset = new Vector3(); + + const rotateStart = new Vector2(); + const rotateEnd = new Vector2(); + const rotateDelta = new Vector2(); + + const panStart = new Vector2(); + const panEnd = new Vector2(); + const panDelta = new Vector2(); + + const dollyStart = new Vector2(); + const dollyEnd = new Vector2(); + const dollyDelta = new Vector2(); + + const dollyDirection = new Vector3(); + const mouse = new Vector2(); + let performCursorZoom = false; + + const pointers = []; + const pointerPositions = {}; + + function getAutoRotationAngle(deltaTime) { + if (deltaTime !== null) { + return ((2 * Math.PI) / 60) * scope.autoRotateSpeed * deltaTime; + } else { + return ((2 * Math.PI) / 60 / 60) * scope.autoRotateSpeed; + } + } + + function getZoomScale() { + return Math.pow(0.95, scope.zoomSpeed); + } + + function rotateLeft(angle) { + sphericalDelta.theta -= angle; + } + + function rotateUp(angle) { + sphericalDelta.phi -= angle; + } + + const panLeft = (function () { + const v = new Vector3(); + + return function panLeft(distance, objectMatrix) { + v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix + v.multiplyScalar(-distance); + + panOffset.add(v); + }; + })(); + + const panUp = (function () { + const v = new Vector3(); + + return function panUp(distance, objectMatrix) { + if (scope.screenSpacePanning === true) { + v.setFromMatrixColumn(objectMatrix, 1); + } else { + v.setFromMatrixColumn(objectMatrix, 0); + v.crossVectors(scope.object.up, v); + } + + v.multiplyScalar(distance); + + panOffset.add(v); + }; + })(); + + // deltaX and deltaY are in pixels; right and down are positive + const pan = (function () { + const offset = new Vector3(); + + return function pan(deltaX, deltaY) { + const element = scope.domElement; + + if (scope.object.isPerspectiveCamera) { + // perspective + const position = scope.object.position; + offset.copy(position).sub(scope.target); + let targetDistance = offset.length(); + + // half of the fov is center to top of screen + targetDistance *= Math.tan( + ((scope.object.fov / 2) * Math.PI) / 180.0 + ); + + // we use only clientHeight here so aspect ratio does not distort speed + panLeft( + (2 * deltaX * targetDistance) / element.clientHeight, + scope.object.matrix + ); + panUp( + (2 * deltaY * targetDistance) / element.clientHeight, + scope.object.matrix + ); + } else if (scope.object.isOrthographicCamera) { + // orthographic + panLeft( + (deltaX * (scope.object.right - scope.object.left)) / + scope.object.zoom / + element.clientWidth, + scope.object.matrix + ); + panUp( + (deltaY * (scope.object.top - scope.object.bottom)) / + scope.object.zoom / + element.clientHeight, + scope.object.matrix + ); + } else { + // camera neither orthographic nor perspective + console.warn( + "WARNING: OrbitControls.js encountered an unknown camera type - pan disabled." + ); + scope.enablePan = false; + } + }; + })(); + + function dollyOut(dollyScale) { + if ( + scope.object.isPerspectiveCamera || + scope.object.isOrthographicCamera + ) { + scale /= dollyScale; + } else { + console.warn( + "WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled." + ); + scope.enableZoom = false; + } + } + + function dollyIn(dollyScale) { + if ( + scope.object.isPerspectiveCamera || + scope.object.isOrthographicCamera + ) { + scale *= dollyScale; + } else { + console.warn( + "WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled." + ); + scope.enableZoom = false; + } + } + + function updateMouseParameters(event) { + if (!scope.zoomToCursor) { + return; + } + + performCursorZoom = true; + + const rect = scope.domElement.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + const w = rect.width; + const h = rect.height; + + mouse.x = (x / w) * 2 - 1; + mouse.y = -(y / h) * 2 + 1; + + dollyDirection + .set(mouse.x, mouse.y, 1) + .unproject(scope.object) + .sub(scope.object.position) + .normalize(); + } + + function clampDistance(dist) { + return Math.max( + scope.minDistance, + Math.min(scope.maxDistance, dist) + ); + } + + // + // event callbacks - update the object state + // + + function handleMouseDownRotate(event) { + rotateStart.set(event.clientX, event.clientY); + } + + function handleMouseDownDolly(event) { + updateMouseParameters(event); + dollyStart.set(event.clientX, event.clientY); + } + + function handleMouseDownPan(event) { + panStart.set(event.clientX, event.clientY); + } + + function handleMouseMoveRotate(event) { + rotateEnd.set(event.clientX, event.clientY); + + rotateDelta + .subVectors(rotateEnd, rotateStart) + .multiplyScalar(scope.rotateSpeed); + + const element = scope.domElement; + + rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight); // yes, height + + rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight); + + rotateStart.copy(rotateEnd); + + scope.update(); + } + + function handleMouseMoveDolly(event) { + dollyEnd.set(event.clientX, event.clientY); + + dollyDelta.subVectors(dollyEnd, dollyStart); + + if (dollyDelta.y > 0) { + dollyOut(getZoomScale()); + } else if (dollyDelta.y < 0) { + dollyIn(getZoomScale()); + } + + dollyStart.copy(dollyEnd); + + scope.update(); + } + + function handleMouseMovePan(event) { + panEnd.set(event.clientX, event.clientY); + + panDelta + .subVectors(panEnd, panStart) + .multiplyScalar(scope.panSpeed); + + pan(panDelta.x, panDelta.y); + + panStart.copy(panEnd); + + scope.update(); + } + + function handleMouseWheel(event) { + updateMouseParameters(event); + + if (event.deltaY < 0) { + dollyIn(getZoomScale()); + } else if (event.deltaY > 0) { + dollyOut(getZoomScale()); + } + + scope.update(); + } + + function handleKeyDown(event) { + let needsUpdate = false; + + switch (event.code) { + case scope.keys.UP: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateUp( + (2 * Math.PI * scope.rotateSpeed) / + scope.domElement.clientHeight + ); + } else { + pan(0, scope.keyPanSpeed); + } + + needsUpdate = true; + break; + + case scope.keys.BOTTOM: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateUp( + (-2 * Math.PI * scope.rotateSpeed) / + scope.domElement.clientHeight + ); + } else { + pan(0, -scope.keyPanSpeed); + } + + needsUpdate = true; + break; + + case scope.keys.LEFT: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateLeft( + (2 * Math.PI * scope.rotateSpeed) / + scope.domElement.clientHeight + ); + } else { + pan(scope.keyPanSpeed, 0); + } + + needsUpdate = true; + break; + + case scope.keys.RIGHT: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateLeft( + (-2 * Math.PI * scope.rotateSpeed) / + scope.domElement.clientHeight + ); + } else { + pan(-scope.keyPanSpeed, 0); + } + + needsUpdate = true; + break; + } + + if (needsUpdate) { + // prevent the browser from scrolling on cursor keys + event.preventDefault(); + + scope.update(); + } + } + + function handleTouchStartRotate() { + if (pointers.length === 1) { + rotateStart.set(pointers[0].pageX, pointers[0].pageY); + } else { + const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); + const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); + + rotateStart.set(x, y); + } + } + + function handleTouchStartPan() { + if (pointers.length === 1) { + panStart.set(pointers[0].pageX, pointers[0].pageY); + } else { + const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); + const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); + + panStart.set(x, y); + } + } + + function handleTouchStartDolly() { + const dx = pointers[0].pageX - pointers[1].pageX; + const dy = pointers[0].pageY - pointers[1].pageY; + + const distance = Math.sqrt(dx * dx + dy * dy); + + dollyStart.set(0, distance); + } + + function handleTouchStartDollyPan() { + if (scope.enableZoom) handleTouchStartDolly(); + + if (scope.enablePan) handleTouchStartPan(); + } + + function handleTouchStartDollyRotate() { + if (scope.enableZoom) handleTouchStartDolly(); + + if (scope.enableRotate) handleTouchStartRotate(); + } + + function handleTouchMoveRotate(event) { + if (pointers.length == 1) { + rotateEnd.set(event.pageX, event.pageY); + } else { + const position = getSecondPointerPosition(event); + + const x = 0.5 * (event.pageX + position.x); + const y = 0.5 * (event.pageY + position.y); + + rotateEnd.set(x, y); + } + + rotateDelta + .subVectors(rotateEnd, rotateStart) + .multiplyScalar(scope.rotateSpeed); + + const element = scope.domElement; + + rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight); // yes, height + + rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight); + + rotateStart.copy(rotateEnd); + } + + function handleTouchMovePan(event) { + if (pointers.length === 1) { + panEnd.set(event.pageX, event.pageY); + } else { + const position = getSecondPointerPosition(event); + + const x = 0.5 * (event.pageX + position.x); + const y = 0.5 * (event.pageY + position.y); + + panEnd.set(x, y); + } + + panDelta + .subVectors(panEnd, panStart) + .multiplyScalar(scope.panSpeed); + + pan(panDelta.x, panDelta.y); + + panStart.copy(panEnd); + } + + function handleTouchMoveDolly(event) { + const position = getSecondPointerPosition(event); + + const dx = event.pageX - position.x; + const dy = event.pageY - position.y; + + const distance = Math.sqrt(dx * dx + dy * dy); + + dollyEnd.set(0, distance); + + dollyDelta.set( + 0, + Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed) + ); + + dollyOut(dollyDelta.y); + + dollyStart.copy(dollyEnd); + } + + function handleTouchMoveDollyPan(event) { + if (scope.enableZoom) handleTouchMoveDolly(event); + + if (scope.enablePan) handleTouchMovePan(event); + } + + function handleTouchMoveDollyRotate(event) { + if (scope.enableZoom) handleTouchMoveDolly(event); + + if (scope.enableRotate) handleTouchMoveRotate(event); + } + + // + // event handlers - FSM: listen for events and reset state + // + + function onPointerDown(event) { + if (scope.enabled === false) return; + + if (pointers.length === 0) { + scope.domElement.setPointerCapture(event.pointerId); + + scope.domElement.addEventListener("pointermove", onPointerMove); + scope.domElement.addEventListener("pointerup", onPointerUp); + } + + // + + addPointer(event); + + if (event.pointerType === "touch") { + onTouchStart(event); + } else { + onMouseDown(event); + } + } + + function onPointerMove(event) { + if (scope.enabled === false) return; + if (!is_in_scene(event)) return; + if (event.pointerType === "touch") { + onTouchMove(event); + } else { + onMouseMove(event); + } + } + + function onPointerUp(event) { + removePointer(event); + + if (pointers.length === 0) { + scope.domElement.releasePointerCapture(event.pointerId); + + scope.domElement.removeEventListener( + "pointermove", + onPointerMove + ); + scope.domElement.removeEventListener("pointerup", onPointerUp); + } + + scope.dispatchEvent(_endEvent); + + state = STATE.NONE; + } + + function onMouseDown(event) { + let mouseAction; + + switch (event.button) { + case 0: + mouseAction = scope.mouseButtons.LEFT; + break; + + case 1: + mouseAction = scope.mouseButtons.MIDDLE; + break; + + case 2: + mouseAction = scope.mouseButtons.RIGHT; + break; + + default: + mouseAction = -1; + } + + switch (mouseAction) { + case MOUSE.DOLLY: + if (scope.enableZoom === false) return; + + handleMouseDownDolly(event); + + state = STATE.DOLLY; + + break; + + case MOUSE.ROTATE: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (scope.enablePan === false) return; + + handleMouseDownPan(event); + + state = STATE.PAN; + } else { + if (scope.enableRotate === false) return; + + handleMouseDownRotate(event); + + state = STATE.ROTATE; + } + + break; + + case MOUSE.PAN: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (scope.enableRotate === false) return; + + handleMouseDownRotate(event); + + state = STATE.ROTATE; + } else { + if (scope.enablePan === false) return; + + handleMouseDownPan(event); + + state = STATE.PAN; + } + + break; + + default: + state = STATE.NONE; + } + + if (state !== STATE.NONE) { + scope.dispatchEvent(_startEvent); + } + } + + function onMouseMove(event) { + switch (state) { + case STATE.ROTATE: + if (scope.enableRotate === false) return; + + handleMouseMoveRotate(event); + + break; + + case STATE.DOLLY: + if (scope.enableZoom === false) return; + + handleMouseMoveDolly(event); + + break; + + case STATE.PAN: + if (scope.enablePan === false) return; + + handleMouseMovePan(event); + + break; + } + } + + function onMouseWheel(event) { + if ( + scope.enabled === false || + scope.enableZoom === false || + state !== STATE.NONE || + !is_in_scene(event) + ) + return; + + event.preventDefault(); + + scope.dispatchEvent(_startEvent); + + handleMouseWheel(event); + + scope.dispatchEvent(_endEvent); + } + + function onKeyDown(event) { + if (scope.enabled === false || scope.enablePan === false) return; + + handleKeyDown(event); + } + + function onTouchStart(event) { + trackPointer(event); + + switch (pointers.length) { + case 1: + switch (scope.touches.ONE) { + case TOUCH.ROTATE: + if (scope.enableRotate === false) return; + + handleTouchStartRotate(); + + state = STATE.TOUCH_ROTATE; + + break; + + case TOUCH.PAN: + if (scope.enablePan === false) return; + + handleTouchStartPan(); + + state = STATE.TOUCH_PAN; + + break; + + default: + state = STATE.NONE; + } + + break; + + case 2: + switch (scope.touches.TWO) { + case TOUCH.DOLLY_PAN: + if ( + scope.enableZoom === false && + scope.enablePan === false + ) + return; + + handleTouchStartDollyPan(); + + state = STATE.TOUCH_DOLLY_PAN; + + break; + + case TOUCH.DOLLY_ROTATE: + if ( + scope.enableZoom === false && + scope.enableRotate === false + ) + return; + + handleTouchStartDollyRotate(); + + state = STATE.TOUCH_DOLLY_ROTATE; + + break; + + default: + state = STATE.NONE; + } + + break; + + default: + state = STATE.NONE; + } + + if (state !== STATE.NONE) { + scope.dispatchEvent(_startEvent); + } + } + + function onTouchMove(event) { + trackPointer(event); + + switch (state) { + case STATE.TOUCH_ROTATE: + if (scope.enableRotate === false) return; + + handleTouchMoveRotate(event); + + scope.update(); + + break; + + case STATE.TOUCH_PAN: + if (scope.enablePan === false) return; + + handleTouchMovePan(event); + + scope.update(); + + break; + + case STATE.TOUCH_DOLLY_PAN: + if (scope.enableZoom === false && scope.enablePan === false) + return; + + handleTouchMoveDollyPan(event); + + scope.update(); + + break; + + case STATE.TOUCH_DOLLY_ROTATE: + if ( + scope.enableZoom === false && + scope.enableRotate === false + ) + return; + + handleTouchMoveDollyRotate(event); + + scope.update(); + + break; + + default: + state = STATE.NONE; + } + } + + function onContextMenu(event) { + if (scope.enabled === false) return; + + event.preventDefault(); + } + + function addPointer(event) { + pointers.push(event); + } + + function removePointer(event) { + delete pointerPositions[event.pointerId]; + + for (let i = 0; i < pointers.length; i++) { + if (pointers[i].pointerId == event.pointerId) { + pointers.splice(i, 1); + return; + } + } + } + + function trackPointer(event) { + let position = pointerPositions[event.pointerId]; + + if (position === undefined) { + position = new Vector2(); + pointerPositions[event.pointerId] = position; + } + + position.set(event.pageX, event.pageY); + } + + function getSecondPointerPosition(event) { + const pointer = + event.pointerId === pointers[0].pointerId + ? pointers[1] + : pointers[0]; + + return pointerPositions[pointer.pointerId]; + } + + // + + scope.domElement.addEventListener("contextmenu", onContextMenu); + + scope.domElement.addEventListener("pointerdown", onPointerDown); + scope.domElement.addEventListener("pointercancel", onPointerUp); + scope.domElement.addEventListener("wheel", onMouseWheel, { + passive: false, + }); + + // force an update at start + + this.update(); + } +} + +export { OrbitControls }; diff --git a/WGLMakie/src/Serialization.js b/WGLMakie/src/Serialization.js index 327ce2515f0..3fc7c66c5a3 100644 --- a/WGLMakie/src/Serialization.js +++ b/WGLMakie/src/Serialization.js @@ -1,5 +1,6 @@ +import * as THREE from "./THREE.js"; import * as Camera from "./Camera.js"; -import * as THREE from "https://cdn.esm.sh/v66/three@0.157/es2021/three.js"; +import { create_line, create_linesegments } from "./Lines.js"; // global scene cache to look them up for dynamic operations in Makie // e.g. insert!(scene, plot) / delete!(scene, plot) @@ -20,6 +21,7 @@ export function delete_scene(scene_id) { if (!scene) { return; } + delete_three_scene(scene); while (scene.children.length > 0) { scene.remove(scene.children[0]); } @@ -39,7 +41,10 @@ export function find_plots(plot_uuids) { export function delete_scenes(scene_uuids, plot_uuids) { plot_uuids.forEach((plot_id) => { - delete plot_cache[plot_id]; + const plot = plot_cache[plot_id]; + if (plot) { + delete_plot(plot); + } }); scene_uuids.forEach((scene_id) => { delete_scene(scene_id); @@ -53,14 +58,9 @@ export function insert_plot(scene_id, plot_data) { }); } -export function delete_plots(scene_id, plot_uuids) { - console.log(`deleting plots!: ${plot_uuids}`) - const scene = find_scene(scene_id); +export function delete_plots(plot_uuids) { const plots = find_plots(plot_uuids); - plots.forEach((p) => { - scene.remove(p); - delete plot_cache[p]; - }); + plots.forEach(delete_plot); } function convert_texture(data) { @@ -120,7 +120,7 @@ function to_uniform(data) { return data; } -function deserialize_uniforms(data) { +export function deserialize_uniforms(data) { const result = {}; // Deno may change constructor names..so... @@ -141,7 +141,16 @@ function deserialize_uniforms(data) { export function deserialize_plot(data) { let mesh; - if ("instance_attributes" in data) { + const update_visible = (v) => { + mesh.visible = v; + // don't return anything, since that will disable on_update callback + return; + }; + if (data.plot_type === "lines") { + mesh = create_line(data); + } else if (data.plot_type === "linesegments") { + mesh = create_linesegments(data); + } else if ("instance_attributes" in data) { mesh = create_instanced_mesh(data); } else { mesh = create_mesh(data); @@ -150,15 +159,12 @@ export function deserialize_plot(data) { mesh.frustumCulled = false; mesh.matrixAutoUpdate = false; mesh.plot_uuid = data.uuid; - const update_visible = (v) => { - mesh.visible = v; - // don't return anything, since that will disable on_update callback - return; - }; update_visible(data.visible.value); data.visible.on(update_visible); connect_uniforms(mesh, data.uniform_updater); - connect_attributes(mesh, data.attribute_updater); + if (!(data.plot_type === "lines" || data.plot_type === "linesegments")) { + connect_attributes(mesh, data.attribute_updater); + } return mesh; } @@ -172,7 +178,6 @@ export function add_plot(scene, plot_data) { // fill in the camera uniforms, that we don't sent in serialization per plot const cam = scene.wgl_camera; const identity = new THREE.Uniform(new THREE.Matrix4()); - if (plot_data.cam_space == "data") { plot_data.uniforms.view = cam.view; plot_data.uniforms.projection = cam.projection; @@ -192,8 +197,9 @@ export function add_plot(scene, plot_data) { plot_data.uniforms.projection = identity; plot_data.uniforms.projectionview = identity; } - + const { px_per_unit } = scene.screen; plot_data.uniforms.resolution = cam.resolution; + plot_data.uniforms.px_per_unit = new THREE.Uniform(px_per_unit); if (plot_data.uniforms.preprojection) { const { space, markerspace } = plot_data; @@ -202,8 +208,25 @@ export function add_plot(scene, plot_data) { markerspace.value ); } + + if (scene.camera_relative_light) { + plot_data.uniforms.light_direction = cam.light_direction; + scene.light_direction.on((value) => { + cam.update_light_dir(value); + }); + } else { + // TODO how update? + const light_dir = new THREE.Vector3().fromArray( + scene.light_direction.value + ); + plot_data.uniforms.light_direction = new THREE.Uniform(light_dir); + scene.light_direction.on((value) => { + plot_data.uniforms.light_direction.value.fromArray(value); + }); + } + const p = deserialize_plot(plot_data); - plot_cache[plot_data.uuid] = p; + plot_cache[p.plot_uuid] = p; scene.add(p); // execute all next insert callbacks const next_insert = new Set(ON_NEXT_INSERT); // copy @@ -253,7 +276,6 @@ function convert_RGB_to_RGBA(rgbArray) { return rgbaArray; } - function create_texture(data) { const buffer = data.data; if (data.size.length == 3) { @@ -304,17 +326,17 @@ function re_create_texture(old_texture, buffer, size) { old_texture.type ); } - tex.minFilter = old_texture.minFilter - tex.magFilter = old_texture.magFilter - tex.anisotropy = old_texture.anisotropy - tex.wrapS = old_texture.wrapS + tex.minFilter = old_texture.minFilter; + tex.magFilter = old_texture.magFilter; + tex.anisotropy = old_texture.anisotropy; + tex.wrapS = old_texture.wrapS; if (size.length > 1) { - tex.wrapT = old_texture.wrapT + tex.wrapT = old_texture.wrapT; } if (size.length > 2) { - tex.wrapR = old_texture.wrapR + tex.wrapR = old_texture.wrapR; } - return tex + return tex; } function BufferAttribute(buffer) { const jsbuff = new THREE.BufferAttribute(buffer.flat, buffer.type_length); @@ -523,41 +545,59 @@ export function deserialize_scene(data, screen) { add_scene(data.uuid, scene); scene.scene_uuid = data.uuid; scene.frustumCulled = false; - scene.pixelarea = data.pixelarea; + scene.viewport = data.viewport; scene.backgroundcolor = data.backgroundcolor; + scene.backgroundcolor_alpha = data.backgroundcolor_alpha; scene.clearscene = data.clearscene; scene.visible = data.visible; + scene.camera_relative_light = data.camera_relative_light; + scene.light_direction = data.light_direction; const camera = new Camera.MakieCamera(); scene.wgl_camera = camera; - function update_cam(camera_matrices) { + function update_cam(camera_matrices, force) { + if (!force) { + // we use the threejs orbit controls, if the julia connection is gone + // at least for 3d ... 2d is still a todo, and will stay static right now + if (!(JSServe.can_send_to_julia && JSServe.can_send_to_julia())) { + return; + } + } const [view, projection, resolution, eyepos] = camera_matrices; camera.update_matrices(view, projection, resolution, eyepos); } - update_cam(data.camera.value); - if (data.cam3d_state) { - Camera.attach_3d_camera(canvas, camera, data.cam3d_state, scene); - } else { - data.camera.on(update_cam); + Camera.attach_3d_camera( + canvas, + camera, + data.cam3d_state, + data.light_direction, + scene + ); } + + update_cam(data.camera.value, true); // force update on first call + camera.update_light_dir(data.light_direction.value); + data.camera.on(update_cam); + data.plots.forEach((plot_data) => { add_plot(scene, plot_data); }); - scene.scene_children = data.children.map((child) => - deserialize_scene(child, screen) - ); + scene.scene_children = data.children.map((child) => { + const childscene = deserialize_scene(child, screen); + return childscene; + }); return scene; } export function delete_plot(plot) { delete plot_cache[plot.plot_uuid]; - const {parent} = plot + const { parent } = plot; if (parent) { - parent.remove(plot) + parent.remove(plot); } plot.geometry.dispose(); plot.material.dispose(); @@ -566,8 +606,8 @@ export function delete_plot(plot) { export function delete_three_scene(scene) { delete scene_cache[scene.scene_uuid]; scene.scene_children.forEach(delete_three_scene); - while(scene.children.length > 0) { - delete_plot(scene.children[0]) + while (scene.children.length > 0) { + delete_plot(scene.children[0]); } } diff --git a/WGLMakie/src/Shaders.js b/WGLMakie/src/Shaders.js new file mode 100644 index 00000000000..4d6959673e9 --- /dev/null +++ b/WGLMakie/src/Shaders.js @@ -0,0 +1,67 @@ +function typedarray_to_vectype(typedArray, ndim) { + if (ndim === 1) { + return "float"; + } else if (typedArray instanceof Float32Array) { + return "vec" + ndim; + } else if (typedArray instanceof Int32Array) { + return "ivec" + ndim; + } else if (typedArray instanceof Uint32Array) { + return "uvec" + ndim; + } else { + return; + } +} + +export function attribute_type(attribute) { + if (attribute) { + return typedarray_to_vectype(attribute.array, attribute.itemSize); + } else { + return; + } +} + +export function uniform_type(obj) { + if (obj instanceof THREE.Uniform) { + return uniform_type(obj.value); + } else if (typeof obj === "number") { + return "float"; + } else if (typeof obj === "boolean") { + return "bool"; + } else if (obj instanceof THREE.Vector2) { + return "vec2"; + } else if (obj instanceof THREE.Vector3) { + return "vec3"; + } else if (obj instanceof THREE.Vector4) { + return "vec4"; + } else if (obj instanceof THREE.Color) { + return "vec4"; + } else if (obj instanceof THREE.Matrix3) { + return "mat3"; + } else if (obj instanceof THREE.Matrix4) { + return "mat4"; + } else if (obj instanceof THREE.Texture) { + return "sampler2D"; + } else { + return; + } +} + +export function uniforms_to_type_declaration(uniform_dict) { + let result = ""; + for (const name in uniform_dict) { + const uniform = uniform_dict[name]; + const type = uniform_type(uniform); + result += `uniform ${type} ${name};\n`; + } + return result; +} + +export function attributes_to_type_declaration(attributes_dict) { + let result = ""; + for (const name in attributes_dict) { + const attribute = attributes_dict[name]; + const type = attribute_type(attribute); + result += `in ${type} ${name};\n`; + } + return result; +} diff --git a/WGLMakie/src/THREE.js b/WGLMakie/src/THREE.js new file mode 100644 index 00000000000..35d525a5b91 --- /dev/null +++ b/WGLMakie/src/THREE.js @@ -0,0 +1,3588 @@ +/* esm.sh - esbuild bundle(three@0.157.0) es2021 production */ +var Hc="157",zx={LEFT:0,MIDDLE:1,RIGHT:2,ROTATE:0,DOLLY:1,PAN:2},Vx={ROTATE:0,PAN:1,DOLLY_PAN:2,DOLLY_ROTATE:3},kd=0,rl=1,Hd=2,kx=3,Hx=0,cd=1,Gd=2,pn=3,Bn=0,Ft=1,gn=2,Gx=2,Dn=0,Wi=1,al=2,ol=3,cl=4,Wd=5,Bi=100,Xd=101,qd=102,ll=103,hl=104,Yd=200,Zd=201,Jd=202,$d=203,ld=204,hd=205,Kd=206,Qd=207,jd=208,ef=209,tf=210,nf=0,sf=1,rf=2,uo=3,af=4,of=5,cf=6,lf=7,xa=0,hf=1,uf=2,Nn=0,df=1,ff=2,pf=3,mf=4,gf=5,Gc=300,zn=301,ci=302,Ir=303,Ur=304,Vs=306,Dr=1e3,It=1001,Nr=1002,pt=1003,fo=1004,Wx=1004,Lr=1005,Xx=1005,mt=1006,ud=1007,qx=1007,li=1008,Yx=1008,On=1009,_f=1010,xf=1011,Wc=1012,dd=1013,Ln=1014,xn=1015,Ts=1016,fd=1017,pd=1018,ii=1020,vf=1021,Wt=1023,yf=1024,Mf=1025,si=1026,Yi=1027,Sf=1028,md=1029,bf=1030,gd=1031,_d=1033,wa=33776,Aa=33777,Ra=33778,Ca=33779,ul=35840,dl=35841,fl=35842,pl=35843,Ef=36196,ml=37492,gl=37496,_l=37808,xl=37809,vl=37810,yl=37811,Ml=37812,Sl=37813,bl=37814,El=37815,Tl=37816,wl=37817,Al=37818,Rl=37819,Cl=37820,Pl=37821,Pa=36492,Ll=36494,Il=36495,Tf=36283,Ul=36284,Dl=36285,Nl=36286,wf=2200,Af=2201,Rf=2202,Or=2300,Fr=2301,La=2302,zi=2400,Vi=2401,Br=2402,Xc=2500,xd=2501,Zx=0,Jx=1,$x=2,vd=3e3,ri=3001,Cf=3200,Pf=3201,mi=0,Lf=1,Xt="",vt="srgb",Mn="srgb-linear",qc="display-p3",va="display-p3-linear",zr="linear",nt="srgb",Vr="rec709",kr="p3",Kx=0,Ia=7680,Qx=7681,jx=7682,ev=7683,tv=34055,nv=34056,iv=5386,sv=512,rv=513,av=514,ov=515,cv=516,lv=517,hv=518,If=519,Uf=512,Df=513,Nf=514,Of=515,Ff=516,Bf=517,zf=518,Vf=519,Hr=35044,uv=35048,dv=35040,fv=35045,pv=35049,mv=35041,gv=35046,_v=35050,xv=35042,vv="100",Ol="300 es",po=1035,vn=2e3,Gr=2001,sn=class{addEventListener(e,t){this._listeners===void 0&&(this._listeners={});let n=this._listeners;n[e]===void 0&&(n[e]=[]),n[e].indexOf(t)===-1&&n[e].push(t)}hasEventListener(e,t){if(this._listeners===void 0)return!1;let n=this._listeners;return n[e]!==void 0&&n[e].indexOf(t)!==-1}removeEventListener(e,t){if(this._listeners===void 0)return;let i=this._listeners[e];if(i!==void 0){let r=i.indexOf(t);r!==-1&&i.splice(r,1)}}dispatchEvent(e){if(this._listeners===void 0)return;let n=this._listeners[e.type];if(n!==void 0){e.target=this;let i=n.slice(0);for(let r=0,a=i.length;r>8&255]+Et[s>>16&255]+Et[s>>24&255]+"-"+Et[e&255]+Et[e>>8&255]+"-"+Et[e>>16&15|64]+Et[e>>24&255]+"-"+Et[t&63|128]+Et[t>>8&255]+"-"+Et[t>>16&255]+Et[t>>24&255]+Et[n&255]+Et[n>>8&255]+Et[n>>16&255]+Et[n>>24&255]).toLowerCase()}function ct(s,e,t){return Math.max(e,Math.min(t,s))}function Yc(s,e){return(s%e+e)%e}function kf(s,e,t,n,i){return n+(s-e)*(i-n)/(t-e)}function Hf(s,e,t){return s!==e?(t-s)/(e-s):0}function ys(s,e,t){return(1-t)*s+t*e}function Gf(s,e,t,n){return ys(s,e,1-Math.exp(-t*n))}function Wf(s,e=1){return e-Math.abs(Yc(s,e*2)-e)}function Xf(s,e,t){return s<=e?0:s>=t?1:(s=(s-e)/(t-e),s*s*(3-2*s))}function qf(s,e,t){return s<=e?0:s>=t?1:(s=(s-e)/(t-e),s*s*s*(s*(s*6-15)+10))}function Yf(s,e){return s+Math.floor(Math.random()*(e-s+1))}function Zf(s,e){return s+Math.random()*(e-s)}function Jf(s){return s*(.5-Math.random())}function $f(s){s!==void 0&&(Fl=s);let e=Fl+=1831565813;return e=Math.imul(e^e>>>15,e|1),e^=e+Math.imul(e^e>>>7,e|61),((e^e>>>14)>>>0)/4294967296}function Kf(s){return s*ai}function Qf(s){return s*Zi}function mo(s){return(s&s-1)===0&&s!==0}function yd(s){return Math.pow(2,Math.ceil(Math.log(s)/Math.LN2))}function Wr(s){return Math.pow(2,Math.floor(Math.log(s)/Math.LN2))}function jf(s,e,t,n,i){let r=Math.cos,a=Math.sin,o=r(t/2),c=a(t/2),l=r((e+n)/2),h=a((e+n)/2),u=r((e-n)/2),d=a((e-n)/2),f=r((n-e)/2),m=a((n-e)/2);switch(i){case"XYX":s.set(o*h,c*u,c*d,o*l);break;case"YZY":s.set(c*d,o*h,c*u,o*l);break;case"ZXZ":s.set(c*u,c*d,o*h,o*l);break;case"XZX":s.set(o*h,c*m,c*f,o*l);break;case"YXY":s.set(c*f,o*h,c*m,o*l);break;case"ZYZ":s.set(c*m,c*f,o*h,o*l);break;default:console.warn("THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: "+i)}}function Ot(s,e){switch(e.constructor){case Float32Array:return s;case Uint32Array:return s/4294967295;case Uint16Array:return s/65535;case Uint8Array:return s/255;case Int32Array:return Math.max(s/2147483647,-1);case Int16Array:return Math.max(s/32767,-1);case Int8Array:return Math.max(s/127,-1);default:throw new Error("Invalid component type.")}}function Be(s,e){switch(e.constructor){case Float32Array:return s;case Uint32Array:return Math.round(s*4294967295);case Uint16Array:return Math.round(s*65535);case Uint8Array:return Math.round(s*255);case Int32Array:return Math.round(s*2147483647);case Int16Array:return Math.round(s*32767);case Int8Array:return Math.round(s*127);default:throw new Error("Invalid component type.")}}var yv={DEG2RAD:ai,RAD2DEG:Zi,generateUUID:kt,clamp:ct,euclideanModulo:Yc,mapLinear:kf,inverseLerp:Hf,lerp:ys,damp:Gf,pingpong:Wf,smoothstep:Xf,smootherstep:qf,randInt:Yf,randFloat:Zf,randFloatSpread:Jf,seededRandom:$f,degToRad:Kf,radToDeg:Qf,isPowerOfTwo:mo,ceilPowerOfTwo:yd,floorPowerOfTwo:Wr,setQuaternionFromProperEuler:jf,normalize:Be,denormalize:Ot},Z=class s{constructor(e=0,t=0){s.prototype.isVector2=!0,this.x=e,this.y=t}get width(){return this.x}set width(e){this.x=e}get height(){return this.y}set height(e){this.y=e}set(e,t){return this.x=e,this.y=t,this}setScalar(e){return this.x=e,this.y=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;default:throw new Error("index is out of range: "+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+e)}}clone(){return new this.constructor(this.x,this.y)}copy(e){return this.x=e.x,this.y=e.y,this}add(e){return this.x+=e.x,this.y+=e.y,this}addScalar(e){return this.x+=e,this.y+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this}subScalar(e){return this.x-=e,this.y-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this}multiply(e){return this.x*=e.x,this.y*=e.y,this}multiplyScalar(e){return this.x*=e,this.y*=e,this}divide(e){return this.x/=e.x,this.y/=e.y,this}divideScalar(e){return this.multiplyScalar(1/e)}applyMatrix3(e){let t=this.x,n=this.y,i=e.elements;return this.x=i[0]*t+i[3]*n+i[6],this.y=i[1]*t+i[4]*n+i[7],this}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this}clamp(e,t){return this.x=Math.max(e.x,Math.min(t.x,this.x)),this.y=Math.max(e.y,Math.min(t.y,this.y)),this}clampScalar(e,t){return this.x=Math.max(e,Math.min(t,this.x)),this.y=Math.max(e,Math.min(t,this.y)),this}clampLength(e,t){let n=this.length();return this.divideScalar(n||1).multiplyScalar(Math.max(e,Math.min(t,n)))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this}negate(){return this.x=-this.x,this.y=-this.y,this}dot(e){return this.x*e.x+this.y*e.y}cross(e){return this.x*e.y-this.y*e.x}lengthSq(){return this.x*this.x+this.y*this.y}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)}normalize(){return this.divideScalar(this.length()||1)}angle(){return Math.atan2(-this.y,-this.x)+Math.PI}angleTo(e){let t=Math.sqrt(this.lengthSq()*e.lengthSq());if(t===0)return Math.PI/2;let n=this.dot(e)/t;return Math.acos(ct(n,-1,1))}distanceTo(e){return Math.sqrt(this.distanceToSquared(e))}distanceToSquared(e){let t=this.x-e.x,n=this.y-e.y;return t*t+n*n}manhattanDistanceTo(e){return Math.abs(this.x-e.x)+Math.abs(this.y-e.y)}setLength(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this}equals(e){return e.x===this.x&&e.y===this.y}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this}rotateAround(e,t){let n=Math.cos(t),i=Math.sin(t),r=this.x-e.x,a=this.y-e.y;return this.x=r*n-a*i+e.x,this.y=r*i+a*n+e.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}},He=class s{constructor(e,t,n,i,r,a,o,c,l){s.prototype.isMatrix3=!0,this.elements=[1,0,0,0,1,0,0,0,1],e!==void 0&&this.set(e,t,n,i,r,a,o,c,l)}set(e,t,n,i,r,a,o,c,l){let h=this.elements;return h[0]=e,h[1]=i,h[2]=o,h[3]=t,h[4]=r,h[5]=c,h[6]=n,h[7]=a,h[8]=l,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(e){let t=this.elements,n=e.elements;return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],this}extractBasis(e,t,n){return e.setFromMatrix3Column(this,0),t.setFromMatrix3Column(this,1),n.setFromMatrix3Column(this,2),this}setFromMatrix4(e){let t=e.elements;return this.set(t[0],t[4],t[8],t[1],t[5],t[9],t[2],t[6],t[10]),this}multiply(e){return this.multiplyMatrices(this,e)}premultiply(e){return this.multiplyMatrices(e,this)}multiplyMatrices(e,t){let n=e.elements,i=t.elements,r=this.elements,a=n[0],o=n[3],c=n[6],l=n[1],h=n[4],u=n[7],d=n[2],f=n[5],m=n[8],_=i[0],g=i[3],p=i[6],v=i[1],x=i[4],y=i[7],b=i[2],w=i[5],R=i[8];return r[0]=a*_+o*v+c*b,r[3]=a*g+o*x+c*w,r[6]=a*p+o*y+c*R,r[1]=l*_+h*v+u*b,r[4]=l*g+h*x+u*w,r[7]=l*p+h*y+u*R,r[2]=d*_+f*v+m*b,r[5]=d*g+f*x+m*w,r[8]=d*p+f*y+m*R,this}multiplyScalar(e){let t=this.elements;return t[0]*=e,t[3]*=e,t[6]*=e,t[1]*=e,t[4]*=e,t[7]*=e,t[2]*=e,t[5]*=e,t[8]*=e,this}determinant(){let e=this.elements,t=e[0],n=e[1],i=e[2],r=e[3],a=e[4],o=e[5],c=e[6],l=e[7],h=e[8];return t*a*h-t*o*l-n*r*h+n*o*c+i*r*l-i*a*c}invert(){let e=this.elements,t=e[0],n=e[1],i=e[2],r=e[3],a=e[4],o=e[5],c=e[6],l=e[7],h=e[8],u=h*a-o*l,d=o*c-h*r,f=l*r-a*c,m=t*u+n*d+i*f;if(m===0)return this.set(0,0,0,0,0,0,0,0,0);let _=1/m;return e[0]=u*_,e[1]=(i*l-h*n)*_,e[2]=(o*n-i*a)*_,e[3]=d*_,e[4]=(h*t-i*c)*_,e[5]=(i*r-o*t)*_,e[6]=f*_,e[7]=(n*c-l*t)*_,e[8]=(a*t-n*r)*_,this}transpose(){let e,t=this.elements;return e=t[1],t[1]=t[3],t[3]=e,e=t[2],t[2]=t[6],t[6]=e,e=t[5],t[5]=t[7],t[7]=e,this}getNormalMatrix(e){return this.setFromMatrix4(e).invert().transpose()}transposeIntoArray(e){let t=this.elements;return e[0]=t[0],e[1]=t[3],e[2]=t[6],e[3]=t[1],e[4]=t[4],e[5]=t[7],e[6]=t[2],e[7]=t[5],e[8]=t[8],this}setUvTransform(e,t,n,i,r,a,o){let c=Math.cos(r),l=Math.sin(r);return this.set(n*c,n*l,-n*(c*a+l*o)+a+e,-i*l,i*c,-i*(-l*a+c*o)+o+t,0,0,1),this}scale(e,t){return this.premultiply(Ua.makeScale(e,t)),this}rotate(e){return this.premultiply(Ua.makeRotation(-e)),this}translate(e,t){return this.premultiply(Ua.makeTranslation(e,t)),this}makeTranslation(e,t){return e.isVector2?this.set(1,0,e.x,0,1,e.y,0,0,1):this.set(1,0,e,0,1,t,0,0,1),this}makeRotation(e){let t=Math.cos(e),n=Math.sin(e);return this.set(t,-n,0,n,t,0,0,0,1),this}makeScale(e,t){return this.set(e,0,0,0,t,0,0,0,1),this}equals(e){let t=this.elements,n=e.elements;for(let i=0;i<9;i++)if(t[i]!==n[i])return!1;return!0}fromArray(e,t=0){for(let n=0;n<9;n++)this.elements[n]=e[n+t];return this}toArray(e=[],t=0){let n=this.elements;return e[t]=n[0],e[t+1]=n[1],e[t+2]=n[2],e[t+3]=n[3],e[t+4]=n[4],e[t+5]=n[5],e[t+6]=n[6],e[t+7]=n[7],e[t+8]=n[8],e}clone(){return new this.constructor().fromArray(this.elements)}},Ua=new He;function Md(s){for(let e=s.length-1;e>=0;--e)if(s[e]>=65535)return!0;return!1}var ep={Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array};function ki(s,e){return new ep[s](e)}function ws(s){return document.createElementNS("http://www.w3.org/1999/xhtml",s)}function tp(){let s=ws("canvas");return s.style.display="block",s}var Bl={};function Ms(s){s in Bl||(Bl[s]=!0,console.warn(s))}var zl=new He().set(.8224621,.177538,0,.0331941,.9668058,0,.0170827,.0723974,.9105199),Vl=new He().set(1.2249401,-.2249404,0,-.0420569,1.0420571,0,-.0196376,-.0786361,1.0982735),Gs={[Mn]:{transfer:zr,primaries:Vr,toReference:s=>s,fromReference:s=>s},[vt]:{transfer:nt,primaries:Vr,toReference:s=>s.convertSRGBToLinear(),fromReference:s=>s.convertLinearToSRGB()},[va]:{transfer:zr,primaries:kr,toReference:s=>s.applyMatrix3(Vl),fromReference:s=>s.applyMatrix3(zl)},[qc]:{transfer:nt,primaries:kr,toReference:s=>s.convertSRGBToLinear().applyMatrix3(Vl),fromReference:s=>s.applyMatrix3(zl).convertLinearToSRGB()}},np=new Set([Mn,va]),Qe={enabled:!0,_workingColorSpace:Mn,get legacyMode(){return console.warn("THREE.ColorManagement: .legacyMode=false renamed to .enabled=true in r150."),!this.enabled},set legacyMode(s){console.warn("THREE.ColorManagement: .legacyMode=false renamed to .enabled=true in r150."),this.enabled=!s},get workingColorSpace(){return this._workingColorSpace},set workingColorSpace(s){if(!np.has(s))throw new Error(`Unsupported working color space, "${s}".`);this._workingColorSpace=s},convert:function(s,e,t){if(this.enabled===!1||e===t||!e||!t)return s;let n=Gs[e].toReference,i=Gs[t].fromReference;return i(n(s))},fromWorkingColorSpace:function(s,e){return this.convert(s,this._workingColorSpace,e)},toWorkingColorSpace:function(s,e){return this.convert(s,e,this._workingColorSpace)},getPrimaries:function(s){return Gs[s].primaries},getTransfer:function(s){return s===Xt?zr:Gs[s].transfer}};function Xi(s){return s<.04045?s*.0773993808:Math.pow(s*.9478672986+.0521327014,2.4)}function Da(s){return s<.0031308?s*12.92:1.055*Math.pow(s,.41666)-.055}var gi,Xr=class{static getDataURL(e){if(/^data:/i.test(e.src)||typeof HTMLCanvasElement>"u")return e.src;let t;if(e instanceof HTMLCanvasElement)t=e;else{gi===void 0&&(gi=ws("canvas")),gi.width=e.width,gi.height=e.height;let n=gi.getContext("2d");e instanceof ImageData?n.putImageData(e,0,0):n.drawImage(e,0,0,e.width,e.height),t=gi}return t.width>2048||t.height>2048?(console.warn("THREE.ImageUtils.getDataURL: Image converted to jpg for performance reasons",e),t.toDataURL("image/jpeg",.6)):t.toDataURL("image/png")}static sRGBToLinear(e){if(typeof HTMLImageElement<"u"&&e instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&e instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&e instanceof ImageBitmap){let t=ws("canvas");t.width=e.width,t.height=e.height;let n=t.getContext("2d");n.drawImage(e,0,0,e.width,e.height);let i=n.getImageData(0,0,e.width,e.height),r=i.data;for(let a=0;a0&&(n.userData=this.userData),t||(e.textures[this.uuid]=n),n}dispose(){this.dispatchEvent({type:"dispose"})}transformUv(e){if(this.mapping!==Gc)return e;if(e.applyMatrix3(this.matrix),e.x<0||e.x>1)switch(this.wrapS){case Dr:e.x=e.x-Math.floor(e.x);break;case It:e.x=e.x<0?0:1;break;case Nr:Math.abs(Math.floor(e.x)%2)===1?e.x=Math.ceil(e.x)-e.x:e.x=e.x-Math.floor(e.x);break}if(e.y<0||e.y>1)switch(this.wrapT){case Dr:e.y=e.y-Math.floor(e.y);break;case It:e.y=e.y<0?0:1;break;case Nr:Math.abs(Math.floor(e.y)%2)===1?e.y=Math.ceil(e.y)-e.y:e.y=e.y-Math.floor(e.y);break}return this.flipY&&(e.y=1-e.y),e}set needsUpdate(e){e===!0&&(this.version++,this.source.needsUpdate=!0)}get encoding(){return Ms("THREE.Texture: Property .encoding has been replaced by .colorSpace."),this.colorSpace===vt?ri:vd}set encoding(e){Ms("THREE.Texture: Property .encoding has been replaced by .colorSpace."),this.colorSpace=e===ri?vt:Xt}};St.DEFAULT_IMAGE=null;St.DEFAULT_MAPPING=Gc;St.DEFAULT_ANISOTROPY=1;var je=class s{constructor(e=0,t=0,n=0,i=1){s.prototype.isVector4=!0,this.x=e,this.y=t,this.z=n,this.w=i}get width(){return this.z}set width(e){this.z=e}get height(){return this.w}set height(e){this.w=e}set(e,t,n,i){return this.x=e,this.y=t,this.z=n,this.w=i,this}setScalar(e){return this.x=e,this.y=e,this.z=e,this.w=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setZ(e){return this.z=e,this}setW(e){return this.w=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;case 2:this.z=t;break;case 3:this.w=t;break;default:throw new Error("index is out of range: "+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw new Error("index is out of range: "+e)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(e){return this.x=e.x,this.y=e.y,this.z=e.z,this.w=e.w!==void 0?e.w:1,this}add(e){return this.x+=e.x,this.y+=e.y,this.z+=e.z,this.w+=e.w,this}addScalar(e){return this.x+=e,this.y+=e,this.z+=e,this.w+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this.z=e.z+t.z,this.w=e.w+t.w,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this.z+=e.z*t,this.w+=e.w*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this.z-=e.z,this.w-=e.w,this}subScalar(e){return this.x-=e,this.y-=e,this.z-=e,this.w-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this.z=e.z-t.z,this.w=e.w-t.w,this}multiply(e){return this.x*=e.x,this.y*=e.y,this.z*=e.z,this.w*=e.w,this}multiplyScalar(e){return this.x*=e,this.y*=e,this.z*=e,this.w*=e,this}applyMatrix4(e){let t=this.x,n=this.y,i=this.z,r=this.w,a=e.elements;return this.x=a[0]*t+a[4]*n+a[8]*i+a[12]*r,this.y=a[1]*t+a[5]*n+a[9]*i+a[13]*r,this.z=a[2]*t+a[6]*n+a[10]*i+a[14]*r,this.w=a[3]*t+a[7]*n+a[11]*i+a[15]*r,this}divideScalar(e){return this.multiplyScalar(1/e)}setAxisAngleFromQuaternion(e){this.w=2*Math.acos(e.w);let t=Math.sqrt(1-e.w*e.w);return t<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=e.x/t,this.y=e.y/t,this.z=e.z/t),this}setAxisAngleFromRotationMatrix(e){let t,n,i,r,c=e.elements,l=c[0],h=c[4],u=c[8],d=c[1],f=c[5],m=c[9],_=c[2],g=c[6],p=c[10];if(Math.abs(h-d)<.01&&Math.abs(u-_)<.01&&Math.abs(m-g)<.01){if(Math.abs(h+d)<.1&&Math.abs(u+_)<.1&&Math.abs(m+g)<.1&&Math.abs(l+f+p-3)<.1)return this.set(1,0,0,0),this;t=Math.PI;let x=(l+1)/2,y=(f+1)/2,b=(p+1)/2,w=(h+d)/4,R=(u+_)/4,I=(m+g)/4;return x>y&&x>b?x<.01?(n=0,i=.707106781,r=.707106781):(n=Math.sqrt(x),i=w/n,r=R/n):y>b?y<.01?(n=.707106781,i=0,r=.707106781):(i=Math.sqrt(y),n=w/i,r=I/i):b<.01?(n=.707106781,i=.707106781,r=0):(r=Math.sqrt(b),n=R/r,i=I/r),this.set(n,i,r,t),this}let v=Math.sqrt((g-m)*(g-m)+(u-_)*(u-_)+(d-h)*(d-h));return Math.abs(v)<.001&&(v=1),this.x=(g-m)/v,this.y=(u-_)/v,this.z=(d-h)/v,this.w=Math.acos((l+f+p-1)/2),this}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this.z=Math.min(this.z,e.z),this.w=Math.min(this.w,e.w),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this.z=Math.max(this.z,e.z),this.w=Math.max(this.w,e.w),this}clamp(e,t){return this.x=Math.max(e.x,Math.min(t.x,this.x)),this.y=Math.max(e.y,Math.min(t.y,this.y)),this.z=Math.max(e.z,Math.min(t.z,this.z)),this.w=Math.max(e.w,Math.min(t.w,this.w)),this}clampScalar(e,t){return this.x=Math.max(e,Math.min(t,this.x)),this.y=Math.max(e,Math.min(t,this.y)),this.z=Math.max(e,Math.min(t,this.z)),this.w=Math.max(e,Math.min(t,this.w)),this}clampLength(e,t){let n=this.length();return this.divideScalar(n||1).multiplyScalar(Math.max(e,Math.min(t,n)))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this.w=Math.floor(this.w),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this.w=Math.ceil(this.w),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this.w=Math.round(this.w),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this.w=Math.trunc(this.w),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this.w=-this.w,this}dot(e){return this.x*e.x+this.y*e.y+this.z*e.z+this.w*e.w}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)+Math.abs(this.w)}normalize(){return this.divideScalar(this.length()||1)}setLength(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this.z+=(e.z-this.z)*t,this.w+=(e.w-this.w)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this.z=e.z+(t.z-e.z)*n,this.w=e.w+(t.w-e.w)*n,this}equals(e){return e.x===this.x&&e.y===this.y&&e.z===this.z&&e.w===this.w}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this.z=e[t+2],this.w=e[t+3],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e[t+2]=this.z,e[t+3]=this.w,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this.z=e.getZ(t),this.w=e.getW(t),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this.w=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z,yield this.w}},go=class extends sn{constructor(e=1,t=1,n={}){super(),this.isRenderTarget=!0,this.width=e,this.height=t,this.depth=1,this.scissor=new je(0,0,e,t),this.scissorTest=!1,this.viewport=new je(0,0,e,t);let i={width:e,height:t,depth:1};n.encoding!==void 0&&(Ms("THREE.WebGLRenderTarget: option.encoding has been replaced by option.colorSpace."),n.colorSpace=n.encoding===ri?vt:Xt),n=Object.assign({generateMipmaps:!1,internalFormat:null,minFilter:mt,depthBuffer:!0,stencilBuffer:!1,depthTexture:null,samples:0},n),this.texture=new St(i,n.mapping,n.wrapS,n.wrapT,n.magFilter,n.minFilter,n.format,n.type,n.anisotropy,n.colorSpace),this.texture.isRenderTargetTexture=!0,this.texture.flipY=!1,this.texture.generateMipmaps=n.generateMipmaps,this.texture.internalFormat=n.internalFormat,this.depthBuffer=n.depthBuffer,this.stencilBuffer=n.stencilBuffer,this.depthTexture=n.depthTexture,this.samples=n.samples}setSize(e,t,n=1){(this.width!==e||this.height!==t||this.depth!==n)&&(this.width=e,this.height=t,this.depth=n,this.texture.image.width=e,this.texture.image.height=t,this.texture.image.depth=n,this.dispose()),this.viewport.set(0,0,e,t),this.scissor.set(0,0,e,t)}clone(){return new this.constructor().copy(this)}copy(e){this.width=e.width,this.height=e.height,this.depth=e.depth,this.scissor.copy(e.scissor),this.scissorTest=e.scissorTest,this.viewport.copy(e.viewport),this.texture=e.texture.clone(),this.texture.isRenderTargetTexture=!0;let t=Object.assign({},e.texture.image);return this.texture.source=new In(t),this.depthBuffer=e.depthBuffer,this.stencilBuffer=e.stencilBuffer,e.depthTexture!==null&&(this.depthTexture=e.depthTexture.clone()),this.samples=e.samples,this}dispose(){this.dispatchEvent({type:"dispose"})}},qt=class extends go{constructor(e=1,t=1,n={}){super(e,t,n),this.isWebGLRenderTarget=!0}},As=class extends St{constructor(e=null,t=1,n=1,i=1){super(null),this.isDataArrayTexture=!0,this.image={data:e,width:t,height:n,depth:i},this.magFilter=pt,this.minFilter=pt,this.wrapR=It,this.generateMipmaps=!1,this.flipY=!1,this.unpackAlignment=1}},kl=class extends qt{constructor(e=1,t=1,n=1){super(e,t),this.isWebGLArrayRenderTarget=!0,this.depth=n,this.texture=new As(null,e,t,n),this.texture.isRenderTargetTexture=!0}},qr=class extends St{constructor(e=null,t=1,n=1,i=1){super(null),this.isData3DTexture=!0,this.image={data:e,width:t,height:n,depth:i},this.magFilter=pt,this.minFilter=pt,this.wrapR=It,this.generateMipmaps=!1,this.flipY=!1,this.unpackAlignment=1}},Hl=class extends qt{constructor(e=1,t=1,n=1){super(e,t),this.isWebGL3DRenderTarget=!0,this.depth=n,this.texture=new qr(null,e,t,n),this.texture.isRenderTargetTexture=!0}},Gl=class extends qt{constructor(e=1,t=1,n=1,i={}){super(e,t,i),this.isWebGLMultipleRenderTargets=!0;let r=this.texture;this.texture=[];for(let a=0;a=0?1:-1,x=1-p*p;if(x>Number.EPSILON){let b=Math.sqrt(x),w=Math.atan2(b,p*v);g=Math.sin(g*w)/b,o=Math.sin(o*w)/b}let y=o*v;if(c=c*g+d*y,l=l*g+f*y,h=h*g+m*y,u=u*g+_*y,g===1-o){let b=1/Math.sqrt(c*c+l*l+h*h+u*u);c*=b,l*=b,h*=b,u*=b}}e[t]=c,e[t+1]=l,e[t+2]=h,e[t+3]=u}static multiplyQuaternionsFlat(e,t,n,i,r,a){let o=n[i],c=n[i+1],l=n[i+2],h=n[i+3],u=r[a],d=r[a+1],f=r[a+2],m=r[a+3];return e[t]=o*m+h*u+c*f-l*d,e[t+1]=c*m+h*d+l*u-o*f,e[t+2]=l*m+h*f+o*d-c*u,e[t+3]=h*m-o*u-c*d-l*f,e}get x(){return this._x}set x(e){this._x=e,this._onChangeCallback()}get y(){return this._y}set y(e){this._y=e,this._onChangeCallback()}get z(){return this._z}set z(e){this._z=e,this._onChangeCallback()}get w(){return this._w}set w(e){this._w=e,this._onChangeCallback()}set(e,t,n,i){return this._x=e,this._y=t,this._z=n,this._w=i,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(e){return this._x=e.x,this._y=e.y,this._z=e.z,this._w=e.w,this._onChangeCallback(),this}setFromEuler(e,t){let n=e._x,i=e._y,r=e._z,a=e._order,o=Math.cos,c=Math.sin,l=o(n/2),h=o(i/2),u=o(r/2),d=c(n/2),f=c(i/2),m=c(r/2);switch(a){case"XYZ":this._x=d*h*u+l*f*m,this._y=l*f*u-d*h*m,this._z=l*h*m+d*f*u,this._w=l*h*u-d*f*m;break;case"YXZ":this._x=d*h*u+l*f*m,this._y=l*f*u-d*h*m,this._z=l*h*m-d*f*u,this._w=l*h*u+d*f*m;break;case"ZXY":this._x=d*h*u-l*f*m,this._y=l*f*u+d*h*m,this._z=l*h*m+d*f*u,this._w=l*h*u-d*f*m;break;case"ZYX":this._x=d*h*u-l*f*m,this._y=l*f*u+d*h*m,this._z=l*h*m-d*f*u,this._w=l*h*u+d*f*m;break;case"YZX":this._x=d*h*u+l*f*m,this._y=l*f*u+d*h*m,this._z=l*h*m-d*f*u,this._w=l*h*u-d*f*m;break;case"XZY":this._x=d*h*u-l*f*m,this._y=l*f*u-d*h*m,this._z=l*h*m+d*f*u,this._w=l*h*u+d*f*m;break;default:console.warn("THREE.Quaternion: .setFromEuler() encountered an unknown order: "+a)}return t!==!1&&this._onChangeCallback(),this}setFromAxisAngle(e,t){let n=t/2,i=Math.sin(n);return this._x=e.x*i,this._y=e.y*i,this._z=e.z*i,this._w=Math.cos(n),this._onChangeCallback(),this}setFromRotationMatrix(e){let t=e.elements,n=t[0],i=t[4],r=t[8],a=t[1],o=t[5],c=t[9],l=t[2],h=t[6],u=t[10],d=n+o+u;if(d>0){let f=.5/Math.sqrt(d+1);this._w=.25/f,this._x=(h-c)*f,this._y=(r-l)*f,this._z=(a-i)*f}else if(n>o&&n>u){let f=2*Math.sqrt(1+n-o-u);this._w=(h-c)/f,this._x=.25*f,this._y=(i+a)/f,this._z=(r+l)/f}else if(o>u){let f=2*Math.sqrt(1+o-n-u);this._w=(r-l)/f,this._x=(i+a)/f,this._y=.25*f,this._z=(c+h)/f}else{let f=2*Math.sqrt(1+u-n-o);this._w=(a-i)/f,this._x=(r+l)/f,this._y=(c+h)/f,this._z=.25*f}return this._onChangeCallback(),this}setFromUnitVectors(e,t){let n=e.dot(t)+1;return nMath.abs(e.z)?(this._x=-e.y,this._y=e.x,this._z=0,this._w=n):(this._x=0,this._y=-e.z,this._z=e.y,this._w=n)):(this._x=e.y*t.z-e.z*t.y,this._y=e.z*t.x-e.x*t.z,this._z=e.x*t.y-e.y*t.x,this._w=n),this.normalize()}angleTo(e){return 2*Math.acos(Math.abs(ct(this.dot(e),-1,1)))}rotateTowards(e,t){let n=this.angleTo(e);if(n===0)return this;let i=Math.min(1,t/n);return this.slerp(e,i),this}identity(){return this.set(0,0,0,1)}invert(){return this.conjugate()}conjugate(){return this._x*=-1,this._y*=-1,this._z*=-1,this._onChangeCallback(),this}dot(e){return this._x*e._x+this._y*e._y+this._z*e._z+this._w*e._w}lengthSq(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w}length(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)}normalize(){let e=this.length();return e===0?(this._x=0,this._y=0,this._z=0,this._w=1):(e=1/e,this._x=this._x*e,this._y=this._y*e,this._z=this._z*e,this._w=this._w*e),this._onChangeCallback(),this}multiply(e){return this.multiplyQuaternions(this,e)}premultiply(e){return this.multiplyQuaternions(e,this)}multiplyQuaternions(e,t){let n=e._x,i=e._y,r=e._z,a=e._w,o=t._x,c=t._y,l=t._z,h=t._w;return this._x=n*h+a*o+i*l-r*c,this._y=i*h+a*c+r*o-n*l,this._z=r*h+a*l+n*c-i*o,this._w=a*h-n*o-i*c-r*l,this._onChangeCallback(),this}slerp(e,t){if(t===0)return this;if(t===1)return this.copy(e);let n=this._x,i=this._y,r=this._z,a=this._w,o=a*e._w+n*e._x+i*e._y+r*e._z;if(o<0?(this._w=-e._w,this._x=-e._x,this._y=-e._y,this._z=-e._z,o=-o):this.copy(e),o>=1)return this._w=a,this._x=n,this._y=i,this._z=r,this;let c=1-o*o;if(c<=Number.EPSILON){let f=1-t;return this._w=f*a+t*this._w,this._x=f*n+t*this._x,this._y=f*i+t*this._y,this._z=f*r+t*this._z,this.normalize(),this._onChangeCallback(),this}let l=Math.sqrt(c),h=Math.atan2(l,o),u=Math.sin((1-t)*h)/l,d=Math.sin(t*h)/l;return this._w=a*u+this._w*d,this._x=n*u+this._x*d,this._y=i*u+this._y*d,this._z=r*u+this._z*d,this._onChangeCallback(),this}slerpQuaternions(e,t,n){return this.copy(e).slerp(t,n)}random(){let e=Math.random(),t=Math.sqrt(1-e),n=Math.sqrt(e),i=2*Math.PI*Math.random(),r=2*Math.PI*Math.random();return this.set(t*Math.cos(i),n*Math.sin(r),n*Math.cos(r),t*Math.sin(i))}equals(e){return e._x===this._x&&e._y===this._y&&e._z===this._z&&e._w===this._w}fromArray(e,t=0){return this._x=e[t],this._y=e[t+1],this._z=e[t+2],this._w=e[t+3],this._onChangeCallback(),this}toArray(e=[],t=0){return e[t]=this._x,e[t+1]=this._y,e[t+2]=this._z,e[t+3]=this._w,e}fromBufferAttribute(e,t){return this._x=e.getX(t),this._y=e.getY(t),this._z=e.getZ(t),this._w=e.getW(t),this}toJSON(){return this.toArray()}_onChange(e){return this._onChangeCallback=e,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._w}},A=class s{constructor(e=0,t=0,n=0){s.prototype.isVector3=!0,this.x=e,this.y=t,this.z=n}set(e,t,n){return n===void 0&&(n=this.z),this.x=e,this.y=t,this.z=n,this}setScalar(e){return this.x=e,this.y=e,this.z=e,this}setX(e){return this.x=e,this}setY(e){return this.y=e,this}setZ(e){return this.z=e,this}setComponent(e,t){switch(e){case 0:this.x=t;break;case 1:this.y=t;break;case 2:this.z=t;break;default:throw new Error("index is out of range: "+e)}return this}getComponent(e){switch(e){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("index is out of range: "+e)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(e){return this.x=e.x,this.y=e.y,this.z=e.z,this}add(e){return this.x+=e.x,this.y+=e.y,this.z+=e.z,this}addScalar(e){return this.x+=e,this.y+=e,this.z+=e,this}addVectors(e,t){return this.x=e.x+t.x,this.y=e.y+t.y,this.z=e.z+t.z,this}addScaledVector(e,t){return this.x+=e.x*t,this.y+=e.y*t,this.z+=e.z*t,this}sub(e){return this.x-=e.x,this.y-=e.y,this.z-=e.z,this}subScalar(e){return this.x-=e,this.y-=e,this.z-=e,this}subVectors(e,t){return this.x=e.x-t.x,this.y=e.y-t.y,this.z=e.z-t.z,this}multiply(e){return this.x*=e.x,this.y*=e.y,this.z*=e.z,this}multiplyScalar(e){return this.x*=e,this.y*=e,this.z*=e,this}multiplyVectors(e,t){return this.x=e.x*t.x,this.y=e.y*t.y,this.z=e.z*t.z,this}applyEuler(e){return this.applyQuaternion(Wl.setFromEuler(e))}applyAxisAngle(e,t){return this.applyQuaternion(Wl.setFromAxisAngle(e,t))}applyMatrix3(e){let t=this.x,n=this.y,i=this.z,r=e.elements;return this.x=r[0]*t+r[3]*n+r[6]*i,this.y=r[1]*t+r[4]*n+r[7]*i,this.z=r[2]*t+r[5]*n+r[8]*i,this}applyNormalMatrix(e){return this.applyMatrix3(e).normalize()}applyMatrix4(e){let t=this.x,n=this.y,i=this.z,r=e.elements,a=1/(r[3]*t+r[7]*n+r[11]*i+r[15]);return this.x=(r[0]*t+r[4]*n+r[8]*i+r[12])*a,this.y=(r[1]*t+r[5]*n+r[9]*i+r[13])*a,this.z=(r[2]*t+r[6]*n+r[10]*i+r[14])*a,this}applyQuaternion(e){let t=this.x,n=this.y,i=this.z,r=e.x,a=e.y,o=e.z,c=e.w,l=c*t+a*i-o*n,h=c*n+o*t-r*i,u=c*i+r*n-a*t,d=-r*t-a*n-o*i;return this.x=l*c+d*-r+h*-o-u*-a,this.y=h*c+d*-a+u*-r-l*-o,this.z=u*c+d*-o+l*-a-h*-r,this}project(e){return this.applyMatrix4(e.matrixWorldInverse).applyMatrix4(e.projectionMatrix)}unproject(e){return this.applyMatrix4(e.projectionMatrixInverse).applyMatrix4(e.matrixWorld)}transformDirection(e){let t=this.x,n=this.y,i=this.z,r=e.elements;return this.x=r[0]*t+r[4]*n+r[8]*i,this.y=r[1]*t+r[5]*n+r[9]*i,this.z=r[2]*t+r[6]*n+r[10]*i,this.normalize()}divide(e){return this.x/=e.x,this.y/=e.y,this.z/=e.z,this}divideScalar(e){return this.multiplyScalar(1/e)}min(e){return this.x=Math.min(this.x,e.x),this.y=Math.min(this.y,e.y),this.z=Math.min(this.z,e.z),this}max(e){return this.x=Math.max(this.x,e.x),this.y=Math.max(this.y,e.y),this.z=Math.max(this.z,e.z),this}clamp(e,t){return this.x=Math.max(e.x,Math.min(t.x,this.x)),this.y=Math.max(e.y,Math.min(t.y,this.y)),this.z=Math.max(e.z,Math.min(t.z,this.z)),this}clampScalar(e,t){return this.x=Math.max(e,Math.min(t,this.x)),this.y=Math.max(e,Math.min(t,this.y)),this.z=Math.max(e,Math.min(t,this.z)),this}clampLength(e,t){let n=this.length();return this.divideScalar(n||1).multiplyScalar(Math.max(e,Math.min(t,n)))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this}dot(e){return this.x*e.x+this.y*e.y+this.z*e.z}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)}normalize(){return this.divideScalar(this.length()||1)}setLength(e){return this.normalize().multiplyScalar(e)}lerp(e,t){return this.x+=(e.x-this.x)*t,this.y+=(e.y-this.y)*t,this.z+=(e.z-this.z)*t,this}lerpVectors(e,t,n){return this.x=e.x+(t.x-e.x)*n,this.y=e.y+(t.y-e.y)*n,this.z=e.z+(t.z-e.z)*n,this}cross(e){return this.crossVectors(this,e)}crossVectors(e,t){let n=e.x,i=e.y,r=e.z,a=t.x,o=t.y,c=t.z;return this.x=i*c-r*o,this.y=r*a-n*c,this.z=n*o-i*a,this}projectOnVector(e){let t=e.lengthSq();if(t===0)return this.set(0,0,0);let n=e.dot(this)/t;return this.copy(e).multiplyScalar(n)}projectOnPlane(e){return Oa.copy(this).projectOnVector(e),this.sub(Oa)}reflect(e){return this.sub(Oa.copy(e).multiplyScalar(2*this.dot(e)))}angleTo(e){let t=Math.sqrt(this.lengthSq()*e.lengthSq());if(t===0)return Math.PI/2;let n=this.dot(e)/t;return Math.acos(ct(n,-1,1))}distanceTo(e){return Math.sqrt(this.distanceToSquared(e))}distanceToSquared(e){let t=this.x-e.x,n=this.y-e.y,i=this.z-e.z;return t*t+n*n+i*i}manhattanDistanceTo(e){return Math.abs(this.x-e.x)+Math.abs(this.y-e.y)+Math.abs(this.z-e.z)}setFromSpherical(e){return this.setFromSphericalCoords(e.radius,e.phi,e.theta)}setFromSphericalCoords(e,t,n){let i=Math.sin(t)*e;return this.x=i*Math.sin(n),this.y=Math.cos(t)*e,this.z=i*Math.cos(n),this}setFromCylindrical(e){return this.setFromCylindricalCoords(e.radius,e.theta,e.y)}setFromCylindricalCoords(e,t,n){return this.x=e*Math.sin(t),this.y=n,this.z=e*Math.cos(t),this}setFromMatrixPosition(e){let t=e.elements;return this.x=t[12],this.y=t[13],this.z=t[14],this}setFromMatrixScale(e){let t=this.setFromMatrixColumn(e,0).length(),n=this.setFromMatrixColumn(e,1).length(),i=this.setFromMatrixColumn(e,2).length();return this.x=t,this.y=n,this.z=i,this}setFromMatrixColumn(e,t){return this.fromArray(e.elements,t*4)}setFromMatrix3Column(e,t){return this.fromArray(e.elements,t*3)}setFromEuler(e){return this.x=e._x,this.y=e._y,this.z=e._z,this}setFromColor(e){return this.x=e.r,this.y=e.g,this.z=e.b,this}equals(e){return e.x===this.x&&e.y===this.y&&e.z===this.z}fromArray(e,t=0){return this.x=e[t],this.y=e[t+1],this.z=e[t+2],this}toArray(e=[],t=0){return e[t]=this.x,e[t+1]=this.y,e[t+2]=this.z,e}fromBufferAttribute(e,t){return this.x=e.getX(t),this.y=e.getY(t),this.z=e.getZ(t),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}randomDirection(){let e=(Math.random()-.5)*2,t=Math.random()*Math.PI*2,n=Math.sqrt(1-e**2);return this.x=n*Math.cos(t),this.y=n*Math.sin(t),this.z=e,this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}},Oa=new A,Wl=new Ut,Qt=class{constructor(e=new A(1/0,1/0,1/0),t=new A(-1/0,-1/0,-1/0)){this.isBox3=!0,this.min=e,this.max=t}set(e,t){return this.min.copy(e),this.max.copy(t),this}setFromArray(e){this.makeEmpty();for(let t=0,n=e.length;tthis.max.x||e.ythis.max.y||e.zthis.max.z)}containsBox(e){return this.min.x<=e.min.x&&e.max.x<=this.max.x&&this.min.y<=e.min.y&&e.max.y<=this.max.y&&this.min.z<=e.min.z&&e.max.z<=this.max.z}getParameter(e,t){return t.set((e.x-this.min.x)/(this.max.x-this.min.x),(e.y-this.min.y)/(this.max.y-this.min.y),(e.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(e){return!(e.max.xthis.max.x||e.max.ythis.max.y||e.max.zthis.max.z)}intersectsSphere(e){return this.clampPoint(e.center,cn),cn.distanceToSquared(e.center)<=e.radius*e.radius}intersectsPlane(e){let t,n;return e.normal.x>0?(t=e.normal.x*this.min.x,n=e.normal.x*this.max.x):(t=e.normal.x*this.max.x,n=e.normal.x*this.min.x),e.normal.y>0?(t+=e.normal.y*this.min.y,n+=e.normal.y*this.max.y):(t+=e.normal.y*this.max.y,n+=e.normal.y*this.min.y),e.normal.z>0?(t+=e.normal.z*this.min.z,n+=e.normal.z*this.max.z):(t+=e.normal.z*this.max.z,n+=e.normal.z*this.min.z),t<=-e.constant&&n>=-e.constant}intersectsTriangle(e){if(this.isEmpty())return!1;this.getCenter(cs),Ws.subVectors(this.max,cs),xi.subVectors(e.a,cs),vi.subVectors(e.b,cs),yi.subVectors(e.c,cs),Tn.subVectors(vi,xi),wn.subVectors(yi,vi),Wn.subVectors(xi,yi);let t=[0,-Tn.z,Tn.y,0,-wn.z,wn.y,0,-Wn.z,Wn.y,Tn.z,0,-Tn.x,wn.z,0,-wn.x,Wn.z,0,-Wn.x,-Tn.y,Tn.x,0,-wn.y,wn.x,0,-Wn.y,Wn.x,0];return!Fa(t,xi,vi,yi,Ws)||(t=[1,0,0,0,1,0,0,0,1],!Fa(t,xi,vi,yi,Ws))?!1:(Xs.crossVectors(Tn,wn),t=[Xs.x,Xs.y,Xs.z],Fa(t,xi,vi,yi,Ws))}clampPoint(e,t){return t.copy(e).clamp(this.min,this.max)}distanceToPoint(e){return this.clampPoint(e,cn).distanceTo(e)}getBoundingSphere(e){return this.isEmpty()?e.makeEmpty():(this.getCenter(e.center),e.radius=this.getSize(cn).length()*.5),e}intersect(e){return this.min.max(e.min),this.max.min(e.max),this.isEmpty()&&this.makeEmpty(),this}union(e){return this.min.min(e.min),this.max.max(e.max),this}applyMatrix4(e){return this.isEmpty()?this:(on[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(e),on[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(e),on[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(e),on[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(e),on[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(e),on[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(e),on[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(e),on[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(e),this.setFromPoints(on),this)}translate(e){return this.min.add(e),this.max.add(e),this}equals(e){return e.min.equals(this.min)&&e.max.equals(this.max)}},on=[new A,new A,new A,new A,new A,new A,new A,new A],cn=new A,_i=new Qt,xi=new A,vi=new A,yi=new A,Tn=new A,wn=new A,Wn=new A,cs=new A,Ws=new A,Xs=new A,Xn=new A;function Fa(s,e,t,n,i){for(let r=0,a=s.length-3;r<=a;r+=3){Xn.fromArray(s,r);let o=i.x*Math.abs(Xn.x)+i.y*Math.abs(Xn.y)+i.z*Math.abs(Xn.z),c=e.dot(Xn),l=t.dot(Xn),h=n.dot(Xn);if(Math.max(-Math.max(c,l,h),Math.min(c,l,h))>o)return!1}return!0}var rp=new Qt,ls=new A,Ba=new A,Yt=class{constructor(e=new A,t=-1){this.center=e,this.radius=t}set(e,t){return this.center.copy(e),this.radius=t,this}setFromPoints(e,t){let n=this.center;t!==void 0?n.copy(t):rp.setFromPoints(e).getCenter(n);let i=0;for(let r=0,a=e.length;rthis.radius*this.radius&&(t.sub(this.center).normalize(),t.multiplyScalar(this.radius).add(this.center)),t}getBoundingBox(e){return this.isEmpty()?(e.makeEmpty(),e):(e.set(this.center,this.center),e.expandByScalar(this.radius),e)}applyMatrix4(e){return this.center.applyMatrix4(e),this.radius=this.radius*e.getMaxScaleOnAxis(),this}translate(e){return this.center.add(e),this}expandByPoint(e){if(this.isEmpty())return this.center.copy(e),this.radius=0,this;ls.subVectors(e,this.center);let t=ls.lengthSq();if(t>this.radius*this.radius){let n=Math.sqrt(t),i=(n-this.radius)*.5;this.center.addScaledVector(ls,i/n),this.radius+=i}return this}union(e){return e.isEmpty()?this:this.isEmpty()?(this.copy(e),this):(this.center.equals(e.center)===!0?this.radius=Math.max(this.radius,e.radius):(Ba.subVectors(e.center,this.center).setLength(e.radius),this.expandByPoint(ls.copy(e.center).add(Ba)),this.expandByPoint(ls.copy(e.center).sub(Ba))),this)}equals(e){return e.center.equals(this.center)&&e.radius===this.radius}clone(){return new this.constructor().copy(this)}},ln=new A,za=new A,qs=new A,An=new A,Va=new A,Ys=new A,ka=new A,hi=class{constructor(e=new A,t=new A(0,0,-1)){this.origin=e,this.direction=t}set(e,t){return this.origin.copy(e),this.direction.copy(t),this}copy(e){return this.origin.copy(e.origin),this.direction.copy(e.direction),this}at(e,t){return t.copy(this.origin).addScaledVector(this.direction,e)}lookAt(e){return this.direction.copy(e).sub(this.origin).normalize(),this}recast(e){return this.origin.copy(this.at(e,ln)),this}closestPointToPoint(e,t){t.subVectors(e,this.origin);let n=t.dot(this.direction);return n<0?t.copy(this.origin):t.copy(this.origin).addScaledVector(this.direction,n)}distanceToPoint(e){return Math.sqrt(this.distanceSqToPoint(e))}distanceSqToPoint(e){let t=ln.subVectors(e,this.origin).dot(this.direction);return t<0?this.origin.distanceToSquared(e):(ln.copy(this.origin).addScaledVector(this.direction,t),ln.distanceToSquared(e))}distanceSqToSegment(e,t,n,i){za.copy(e).add(t).multiplyScalar(.5),qs.copy(t).sub(e).normalize(),An.copy(this.origin).sub(za);let r=e.distanceTo(t)*.5,a=-this.direction.dot(qs),o=An.dot(this.direction),c=-An.dot(qs),l=An.lengthSq(),h=Math.abs(1-a*a),u,d,f,m;if(h>0)if(u=a*c-o,d=a*o-c,m=r*h,u>=0)if(d>=-m)if(d<=m){let _=1/h;u*=_,d*=_,f=u*(u+a*d+2*o)+d*(a*u+d+2*c)+l}else d=r,u=Math.max(0,-(a*d+o)),f=-u*u+d*(d+2*c)+l;else d=-r,u=Math.max(0,-(a*d+o)),f=-u*u+d*(d+2*c)+l;else d<=-m?(u=Math.max(0,-(-a*r+o)),d=u>0?-r:Math.min(Math.max(-r,-c),r),f=-u*u+d*(d+2*c)+l):d<=m?(u=0,d=Math.min(Math.max(-r,-c),r),f=d*(d+2*c)+l):(u=Math.max(0,-(a*r+o)),d=u>0?r:Math.min(Math.max(-r,-c),r),f=-u*u+d*(d+2*c)+l);else d=a>0?-r:r,u=Math.max(0,-(a*d+o)),f=-u*u+d*(d+2*c)+l;return n&&n.copy(this.origin).addScaledVector(this.direction,u),i&&i.copy(za).addScaledVector(qs,d),f}intersectSphere(e,t){ln.subVectors(e.center,this.origin);let n=ln.dot(this.direction),i=ln.dot(ln)-n*n,r=e.radius*e.radius;if(i>r)return null;let a=Math.sqrt(r-i),o=n-a,c=n+a;return c<0?null:o<0?this.at(c,t):this.at(o,t)}intersectsSphere(e){return this.distanceSqToPoint(e.center)<=e.radius*e.radius}distanceToPlane(e){let t=e.normal.dot(this.direction);if(t===0)return e.distanceToPoint(this.origin)===0?0:null;let n=-(this.origin.dot(e.normal)+e.constant)/t;return n>=0?n:null}intersectPlane(e,t){let n=this.distanceToPlane(e);return n===null?null:this.at(n,t)}intersectsPlane(e){let t=e.distanceToPoint(this.origin);return t===0||e.normal.dot(this.direction)*t<0}intersectBox(e,t){let n,i,r,a,o,c,l=1/this.direction.x,h=1/this.direction.y,u=1/this.direction.z,d=this.origin;return l>=0?(n=(e.min.x-d.x)*l,i=(e.max.x-d.x)*l):(n=(e.max.x-d.x)*l,i=(e.min.x-d.x)*l),h>=0?(r=(e.min.y-d.y)*h,a=(e.max.y-d.y)*h):(r=(e.max.y-d.y)*h,a=(e.min.y-d.y)*h),n>a||r>i||((r>n||isNaN(n))&&(n=r),(a=0?(o=(e.min.z-d.z)*u,c=(e.max.z-d.z)*u):(o=(e.max.z-d.z)*u,c=(e.min.z-d.z)*u),n>c||o>i)||((o>n||n!==n)&&(n=o),(c=0?n:i,t)}intersectsBox(e){return this.intersectBox(e,ln)!==null}intersectTriangle(e,t,n,i,r){Va.subVectors(t,e),Ys.subVectors(n,e),ka.crossVectors(Va,Ys);let a=this.direction.dot(ka),o;if(a>0){if(i)return null;o=1}else if(a<0)o=-1,a=-a;else return null;An.subVectors(this.origin,e);let c=o*this.direction.dot(Ys.crossVectors(An,Ys));if(c<0)return null;let l=o*this.direction.dot(Va.cross(An));if(l<0||c+l>a)return null;let h=-o*An.dot(ka);return h<0?null:this.at(h/a,r)}applyMatrix4(e){return this.origin.applyMatrix4(e),this.direction.transformDirection(e),this}equals(e){return e.origin.equals(this.origin)&&e.direction.equals(this.direction)}clone(){return new this.constructor().copy(this)}},ze=class s{constructor(e,t,n,i,r,a,o,c,l,h,u,d,f,m,_,g){s.prototype.isMatrix4=!0,this.elements=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],e!==void 0&&this.set(e,t,n,i,r,a,o,c,l,h,u,d,f,m,_,g)}set(e,t,n,i,r,a,o,c,l,h,u,d,f,m,_,g){let p=this.elements;return p[0]=e,p[4]=t,p[8]=n,p[12]=i,p[1]=r,p[5]=a,p[9]=o,p[13]=c,p[2]=l,p[6]=h,p[10]=u,p[14]=d,p[3]=f,p[7]=m,p[11]=_,p[15]=g,this}identity(){return this.set(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1),this}clone(){return new s().fromArray(this.elements)}copy(e){let t=this.elements,n=e.elements;return t[0]=n[0],t[1]=n[1],t[2]=n[2],t[3]=n[3],t[4]=n[4],t[5]=n[5],t[6]=n[6],t[7]=n[7],t[8]=n[8],t[9]=n[9],t[10]=n[10],t[11]=n[11],t[12]=n[12],t[13]=n[13],t[14]=n[14],t[15]=n[15],this}copyPosition(e){let t=this.elements,n=e.elements;return t[12]=n[12],t[13]=n[13],t[14]=n[14],this}setFromMatrix3(e){let t=e.elements;return this.set(t[0],t[3],t[6],0,t[1],t[4],t[7],0,t[2],t[5],t[8],0,0,0,0,1),this}extractBasis(e,t,n){return e.setFromMatrixColumn(this,0),t.setFromMatrixColumn(this,1),n.setFromMatrixColumn(this,2),this}makeBasis(e,t,n){return this.set(e.x,t.x,n.x,0,e.y,t.y,n.y,0,e.z,t.z,n.z,0,0,0,0,1),this}extractRotation(e){let t=this.elements,n=e.elements,i=1/Mi.setFromMatrixColumn(e,0).length(),r=1/Mi.setFromMatrixColumn(e,1).length(),a=1/Mi.setFromMatrixColumn(e,2).length();return t[0]=n[0]*i,t[1]=n[1]*i,t[2]=n[2]*i,t[3]=0,t[4]=n[4]*r,t[5]=n[5]*r,t[6]=n[6]*r,t[7]=0,t[8]=n[8]*a,t[9]=n[9]*a,t[10]=n[10]*a,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,this}makeRotationFromEuler(e){let t=this.elements,n=e.x,i=e.y,r=e.z,a=Math.cos(n),o=Math.sin(n),c=Math.cos(i),l=Math.sin(i),h=Math.cos(r),u=Math.sin(r);if(e.order==="XYZ"){let d=a*h,f=a*u,m=o*h,_=o*u;t[0]=c*h,t[4]=-c*u,t[8]=l,t[1]=f+m*l,t[5]=d-_*l,t[9]=-o*c,t[2]=_-d*l,t[6]=m+f*l,t[10]=a*c}else if(e.order==="YXZ"){let d=c*h,f=c*u,m=l*h,_=l*u;t[0]=d+_*o,t[4]=m*o-f,t[8]=a*l,t[1]=a*u,t[5]=a*h,t[9]=-o,t[2]=f*o-m,t[6]=_+d*o,t[10]=a*c}else if(e.order==="ZXY"){let d=c*h,f=c*u,m=l*h,_=l*u;t[0]=d-_*o,t[4]=-a*u,t[8]=m+f*o,t[1]=f+m*o,t[5]=a*h,t[9]=_-d*o,t[2]=-a*l,t[6]=o,t[10]=a*c}else if(e.order==="ZYX"){let d=a*h,f=a*u,m=o*h,_=o*u;t[0]=c*h,t[4]=m*l-f,t[8]=d*l+_,t[1]=c*u,t[5]=_*l+d,t[9]=f*l-m,t[2]=-l,t[6]=o*c,t[10]=a*c}else if(e.order==="YZX"){let d=a*c,f=a*l,m=o*c,_=o*l;t[0]=c*h,t[4]=_-d*u,t[8]=m*u+f,t[1]=u,t[5]=a*h,t[9]=-o*h,t[2]=-l*h,t[6]=f*u+m,t[10]=d-_*u}else if(e.order==="XZY"){let d=a*c,f=a*l,m=o*c,_=o*l;t[0]=c*h,t[4]=-u,t[8]=l*h,t[1]=d*u+_,t[5]=a*h,t[9]=f*u-m,t[2]=m*u-f,t[6]=o*h,t[10]=_*u+d}return t[3]=0,t[7]=0,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,this}makeRotationFromQuaternion(e){return this.compose(ap,e,op)}lookAt(e,t,n){let i=this.elements;return zt.subVectors(e,t),zt.lengthSq()===0&&(zt.z=1),zt.normalize(),Rn.crossVectors(n,zt),Rn.lengthSq()===0&&(Math.abs(n.z)===1?zt.x+=1e-4:zt.z+=1e-4,zt.normalize(),Rn.crossVectors(n,zt)),Rn.normalize(),Zs.crossVectors(zt,Rn),i[0]=Rn.x,i[4]=Zs.x,i[8]=zt.x,i[1]=Rn.y,i[5]=Zs.y,i[9]=zt.y,i[2]=Rn.z,i[6]=Zs.z,i[10]=zt.z,this}multiply(e){return this.multiplyMatrices(this,e)}premultiply(e){return this.multiplyMatrices(e,this)}multiplyMatrices(e,t){let n=e.elements,i=t.elements,r=this.elements,a=n[0],o=n[4],c=n[8],l=n[12],h=n[1],u=n[5],d=n[9],f=n[13],m=n[2],_=n[6],g=n[10],p=n[14],v=n[3],x=n[7],y=n[11],b=n[15],w=i[0],R=i[4],I=i[8],M=i[12],T=i[1],O=i[5],Y=i[9],$=i[13],U=i[2],z=i[6],q=i[10],H=i[14],ne=i[3],W=i[7],K=i[11],D=i[15];return r[0]=a*w+o*T+c*U+l*ne,r[4]=a*R+o*O+c*z+l*W,r[8]=a*I+o*Y+c*q+l*K,r[12]=a*M+o*$+c*H+l*D,r[1]=h*w+u*T+d*U+f*ne,r[5]=h*R+u*O+d*z+f*W,r[9]=h*I+u*Y+d*q+f*K,r[13]=h*M+u*$+d*H+f*D,r[2]=m*w+_*T+g*U+p*ne,r[6]=m*R+_*O+g*z+p*W,r[10]=m*I+_*Y+g*q+p*K,r[14]=m*M+_*$+g*H+p*D,r[3]=v*w+x*T+y*U+b*ne,r[7]=v*R+x*O+y*z+b*W,r[11]=v*I+x*Y+y*q+b*K,r[15]=v*M+x*$+y*H+b*D,this}multiplyScalar(e){let t=this.elements;return t[0]*=e,t[4]*=e,t[8]*=e,t[12]*=e,t[1]*=e,t[5]*=e,t[9]*=e,t[13]*=e,t[2]*=e,t[6]*=e,t[10]*=e,t[14]*=e,t[3]*=e,t[7]*=e,t[11]*=e,t[15]*=e,this}determinant(){let e=this.elements,t=e[0],n=e[4],i=e[8],r=e[12],a=e[1],o=e[5],c=e[9],l=e[13],h=e[2],u=e[6],d=e[10],f=e[14],m=e[3],_=e[7],g=e[11],p=e[15];return m*(+r*c*u-i*l*u-r*o*d+n*l*d+i*o*f-n*c*f)+_*(+t*c*f-t*l*d+r*a*d-i*a*f+i*l*h-r*c*h)+g*(+t*l*u-t*o*f-r*a*u+n*a*f+r*o*h-n*l*h)+p*(-i*o*h-t*c*u+t*o*d+i*a*u-n*a*d+n*c*h)}transpose(){let e=this.elements,t;return t=e[1],e[1]=e[4],e[4]=t,t=e[2],e[2]=e[8],e[8]=t,t=e[6],e[6]=e[9],e[9]=t,t=e[3],e[3]=e[12],e[12]=t,t=e[7],e[7]=e[13],e[13]=t,t=e[11],e[11]=e[14],e[14]=t,this}setPosition(e,t,n){let i=this.elements;return e.isVector3?(i[12]=e.x,i[13]=e.y,i[14]=e.z):(i[12]=e,i[13]=t,i[14]=n),this}invert(){let e=this.elements,t=e[0],n=e[1],i=e[2],r=e[3],a=e[4],o=e[5],c=e[6],l=e[7],h=e[8],u=e[9],d=e[10],f=e[11],m=e[12],_=e[13],g=e[14],p=e[15],v=u*g*l-_*d*l+_*c*f-o*g*f-u*c*p+o*d*p,x=m*d*l-h*g*l-m*c*f+a*g*f+h*c*p-a*d*p,y=h*_*l-m*u*l+m*o*f-a*_*f-h*o*p+a*u*p,b=m*u*c-h*_*c-m*o*d+a*_*d+h*o*g-a*u*g,w=t*v+n*x+i*y+r*b;if(w===0)return this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);let R=1/w;return e[0]=v*R,e[1]=(_*d*r-u*g*r-_*i*f+n*g*f+u*i*p-n*d*p)*R,e[2]=(o*g*r-_*c*r+_*i*l-n*g*l-o*i*p+n*c*p)*R,e[3]=(u*c*r-o*d*r-u*i*l+n*d*l+o*i*f-n*c*f)*R,e[4]=x*R,e[5]=(h*g*r-m*d*r+m*i*f-t*g*f-h*i*p+t*d*p)*R,e[6]=(m*c*r-a*g*r-m*i*l+t*g*l+a*i*p-t*c*p)*R,e[7]=(a*d*r-h*c*r+h*i*l-t*d*l-a*i*f+t*c*f)*R,e[8]=y*R,e[9]=(m*u*r-h*_*r-m*n*f+t*_*f+h*n*p-t*u*p)*R,e[10]=(a*_*r-m*o*r+m*n*l-t*_*l-a*n*p+t*o*p)*R,e[11]=(h*o*r-a*u*r-h*n*l+t*u*l+a*n*f-t*o*f)*R,e[12]=b*R,e[13]=(h*_*i-m*u*i+m*n*d-t*_*d-h*n*g+t*u*g)*R,e[14]=(m*o*i-a*_*i-m*n*c+t*_*c+a*n*g-t*o*g)*R,e[15]=(a*u*i-h*o*i+h*n*c-t*u*c-a*n*d+t*o*d)*R,this}scale(e){let t=this.elements,n=e.x,i=e.y,r=e.z;return t[0]*=n,t[4]*=i,t[8]*=r,t[1]*=n,t[5]*=i,t[9]*=r,t[2]*=n,t[6]*=i,t[10]*=r,t[3]*=n,t[7]*=i,t[11]*=r,this}getMaxScaleOnAxis(){let e=this.elements,t=e[0]*e[0]+e[1]*e[1]+e[2]*e[2],n=e[4]*e[4]+e[5]*e[5]+e[6]*e[6],i=e[8]*e[8]+e[9]*e[9]+e[10]*e[10];return Math.sqrt(Math.max(t,n,i))}makeTranslation(e,t,n){return e.isVector3?this.set(1,0,0,e.x,0,1,0,e.y,0,0,1,e.z,0,0,0,1):this.set(1,0,0,e,0,1,0,t,0,0,1,n,0,0,0,1),this}makeRotationX(e){let t=Math.cos(e),n=Math.sin(e);return this.set(1,0,0,0,0,t,-n,0,0,n,t,0,0,0,0,1),this}makeRotationY(e){let t=Math.cos(e),n=Math.sin(e);return this.set(t,0,n,0,0,1,0,0,-n,0,t,0,0,0,0,1),this}makeRotationZ(e){let t=Math.cos(e),n=Math.sin(e);return this.set(t,-n,0,0,n,t,0,0,0,0,1,0,0,0,0,1),this}makeRotationAxis(e,t){let n=Math.cos(t),i=Math.sin(t),r=1-n,a=e.x,o=e.y,c=e.z,l=r*a,h=r*o;return this.set(l*a+n,l*o-i*c,l*c+i*o,0,l*o+i*c,h*o+n,h*c-i*a,0,l*c-i*o,h*c+i*a,r*c*c+n,0,0,0,0,1),this}makeScale(e,t,n){return this.set(e,0,0,0,0,t,0,0,0,0,n,0,0,0,0,1),this}makeShear(e,t,n,i,r,a){return this.set(1,n,r,0,e,1,a,0,t,i,1,0,0,0,0,1),this}compose(e,t,n){let i=this.elements,r=t._x,a=t._y,o=t._z,c=t._w,l=r+r,h=a+a,u=o+o,d=r*l,f=r*h,m=r*u,_=a*h,g=a*u,p=o*u,v=c*l,x=c*h,y=c*u,b=n.x,w=n.y,R=n.z;return i[0]=(1-(_+p))*b,i[1]=(f+y)*b,i[2]=(m-x)*b,i[3]=0,i[4]=(f-y)*w,i[5]=(1-(d+p))*w,i[6]=(g+v)*w,i[7]=0,i[8]=(m+x)*R,i[9]=(g-v)*R,i[10]=(1-(d+_))*R,i[11]=0,i[12]=e.x,i[13]=e.y,i[14]=e.z,i[15]=1,this}decompose(e,t,n){let i=this.elements,r=Mi.set(i[0],i[1],i[2]).length(),a=Mi.set(i[4],i[5],i[6]).length(),o=Mi.set(i[8],i[9],i[10]).length();this.determinant()<0&&(r=-r),e.x=i[12],e.y=i[13],e.z=i[14],$t.copy(this);let l=1/r,h=1/a,u=1/o;return $t.elements[0]*=l,$t.elements[1]*=l,$t.elements[2]*=l,$t.elements[4]*=h,$t.elements[5]*=h,$t.elements[6]*=h,$t.elements[8]*=u,$t.elements[9]*=u,$t.elements[10]*=u,t.setFromRotationMatrix($t),n.x=r,n.y=a,n.z=o,this}makePerspective(e,t,n,i,r,a,o=vn){let c=this.elements,l=2*r/(t-e),h=2*r/(n-i),u=(t+e)/(t-e),d=(n+i)/(n-i),f,m;if(o===vn)f=-(a+r)/(a-r),m=-2*a*r/(a-r);else if(o===Gr)f=-a/(a-r),m=-a*r/(a-r);else throw new Error("THREE.Matrix4.makePerspective(): Invalid coordinate system: "+o);return c[0]=l,c[4]=0,c[8]=u,c[12]=0,c[1]=0,c[5]=h,c[9]=d,c[13]=0,c[2]=0,c[6]=0,c[10]=f,c[14]=m,c[3]=0,c[7]=0,c[11]=-1,c[15]=0,this}makeOrthographic(e,t,n,i,r,a,o=vn){let c=this.elements,l=1/(t-e),h=1/(n-i),u=1/(a-r),d=(t+e)*l,f=(n+i)*h,m,_;if(o===vn)m=(a+r)*u,_=-2*u;else if(o===Gr)m=r*u,_=-1*u;else throw new Error("THREE.Matrix4.makeOrthographic(): Invalid coordinate system: "+o);return c[0]=2*l,c[4]=0,c[8]=0,c[12]=-d,c[1]=0,c[5]=2*h,c[9]=0,c[13]=-f,c[2]=0,c[6]=0,c[10]=_,c[14]=-m,c[3]=0,c[7]=0,c[11]=0,c[15]=1,this}equals(e){let t=this.elements,n=e.elements;for(let i=0;i<16;i++)if(t[i]!==n[i])return!1;return!0}fromArray(e,t=0){for(let n=0;n<16;n++)this.elements[n]=e[n+t];return this}toArray(e=[],t=0){let n=this.elements;return e[t]=n[0],e[t+1]=n[1],e[t+2]=n[2],e[t+3]=n[3],e[t+4]=n[4],e[t+5]=n[5],e[t+6]=n[6],e[t+7]=n[7],e[t+8]=n[8],e[t+9]=n[9],e[t+10]=n[10],e[t+11]=n[11],e[t+12]=n[12],e[t+13]=n[13],e[t+14]=n[14],e[t+15]=n[15],e}},Mi=new A,$t=new ze,ap=new A(0,0,0),op=new A(1,1,1),Rn=new A,Zs=new A,zt=new A,Xl=new ze,ql=new Ut,Yr=class s{constructor(e=0,t=0,n=0,i=s.DEFAULT_ORDER){this.isEuler=!0,this._x=e,this._y=t,this._z=n,this._order=i}get x(){return this._x}set x(e){this._x=e,this._onChangeCallback()}get y(){return this._y}set y(e){this._y=e,this._onChangeCallback()}get z(){return this._z}set z(e){this._z=e,this._onChangeCallback()}get order(){return this._order}set order(e){this._order=e,this._onChangeCallback()}set(e,t,n,i=this._order){return this._x=e,this._y=t,this._z=n,this._order=i,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._order)}copy(e){return this._x=e._x,this._y=e._y,this._z=e._z,this._order=e._order,this._onChangeCallback(),this}setFromRotationMatrix(e,t=this._order,n=!0){let i=e.elements,r=i[0],a=i[4],o=i[8],c=i[1],l=i[5],h=i[9],u=i[2],d=i[6],f=i[10];switch(t){case"XYZ":this._y=Math.asin(ct(o,-1,1)),Math.abs(o)<.9999999?(this._x=Math.atan2(-h,f),this._z=Math.atan2(-a,r)):(this._x=Math.atan2(d,l),this._z=0);break;case"YXZ":this._x=Math.asin(-ct(h,-1,1)),Math.abs(h)<.9999999?(this._y=Math.atan2(o,f),this._z=Math.atan2(c,l)):(this._y=Math.atan2(-u,r),this._z=0);break;case"ZXY":this._x=Math.asin(ct(d,-1,1)),Math.abs(d)<.9999999?(this._y=Math.atan2(-u,f),this._z=Math.atan2(-a,l)):(this._y=0,this._z=Math.atan2(c,r));break;case"ZYX":this._y=Math.asin(-ct(u,-1,1)),Math.abs(u)<.9999999?(this._x=Math.atan2(d,f),this._z=Math.atan2(c,r)):(this._x=0,this._z=Math.atan2(-a,l));break;case"YZX":this._z=Math.asin(ct(c,-1,1)),Math.abs(c)<.9999999?(this._x=Math.atan2(-h,l),this._y=Math.atan2(-u,r)):(this._x=0,this._y=Math.atan2(o,f));break;case"XZY":this._z=Math.asin(-ct(a,-1,1)),Math.abs(a)<.9999999?(this._x=Math.atan2(d,l),this._y=Math.atan2(o,r)):(this._x=Math.atan2(-h,f),this._y=0);break;default:console.warn("THREE.Euler: .setFromRotationMatrix() encountered an unknown order: "+t)}return this._order=t,n===!0&&this._onChangeCallback(),this}setFromQuaternion(e,t,n){return Xl.makeRotationFromQuaternion(e),this.setFromRotationMatrix(Xl,t,n)}setFromVector3(e,t=this._order){return this.set(e.x,e.y,e.z,t)}reorder(e){return ql.setFromEuler(this),this.setFromQuaternion(ql,e)}equals(e){return e._x===this._x&&e._y===this._y&&e._z===this._z&&e._order===this._order}fromArray(e){return this._x=e[0],this._y=e[1],this._z=e[2],e[3]!==void 0&&(this._order=e[3]),this._onChangeCallback(),this}toArray(e=[],t=0){return e[t]=this._x,e[t+1]=this._y,e[t+2]=this._z,e[t+3]=this._order,e}_onChange(e){return this._onChangeCallback=e,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._order}};Yr.DEFAULT_ORDER="XYZ";var Rs=class{constructor(){this.mask=1}set(e){this.mask=(1<>>0}enable(e){this.mask|=1<1){for(let t=0;t1){for(let n=0;n0&&(n=n.concat(a))}return n}getWorldPosition(e){return this.updateWorldMatrix(!0,!1),e.setFromMatrixPosition(this.matrixWorld)}getWorldQuaternion(e){return this.updateWorldMatrix(!0,!1),this.matrixWorld.decompose(hs,e,lp),e}getWorldScale(e){return this.updateWorldMatrix(!0,!1),this.matrixWorld.decompose(hs,hp,e),e}getWorldDirection(e){this.updateWorldMatrix(!0,!1);let t=this.matrixWorld.elements;return e.set(t[8],t[9],t[10]).normalize()}raycast(){}traverse(e){e(this);let t=this.children;for(let n=0,i=t.length;n0&&(i.userData=this.userData),i.layers=this.layers.mask,i.matrix=this.matrix.toArray(),i.up=this.up.toArray(),this.matrixAutoUpdate===!1&&(i.matrixAutoUpdate=!1),this.isInstancedMesh&&(i.type="InstancedMesh",i.count=this.count,i.instanceMatrix=this.instanceMatrix.toJSON(),this.instanceColor!==null&&(i.instanceColor=this.instanceColor.toJSON()));function r(o,c){return o[c.uuid]===void 0&&(o[c.uuid]=c.toJSON(e)),c.uuid}if(this.isScene)this.background&&(this.background.isColor?i.background=this.background.toJSON():this.background.isTexture&&(i.background=this.background.toJSON(e).uuid)),this.environment&&this.environment.isTexture&&this.environment.isRenderTargetTexture!==!0&&(i.environment=this.environment.toJSON(e).uuid);else if(this.isMesh||this.isLine||this.isPoints){i.geometry=r(e.geometries,this.geometry);let o=this.geometry.parameters;if(o!==void 0&&o.shapes!==void 0){let c=o.shapes;if(Array.isArray(c))for(let l=0,h=c.length;l0){i.children=[];for(let o=0;o0){i.animations=[];for(let o=0;o0&&(n.geometries=o),c.length>0&&(n.materials=c),l.length>0&&(n.textures=l),h.length>0&&(n.images=h),u.length>0&&(n.shapes=u),d.length>0&&(n.skeletons=d),f.length>0&&(n.animations=f),m.length>0&&(n.nodes=m)}return n.object=i,n;function a(o){let c=[];for(let l in o){let h=o[l];delete h.metadata,c.push(h)}return c}}clone(e){return new this.constructor().copy(this,e)}copy(e,t=!0){if(this.name=e.name,this.up.copy(e.up),this.position.copy(e.position),this.rotation.order=e.rotation.order,this.quaternion.copy(e.quaternion),this.scale.copy(e.scale),this.matrix.copy(e.matrix),this.matrixWorld.copy(e.matrixWorld),this.matrixAutoUpdate=e.matrixAutoUpdate,this.matrixWorldNeedsUpdate=e.matrixWorldNeedsUpdate,this.matrixWorldAutoUpdate=e.matrixWorldAutoUpdate,this.layers.mask=e.layers.mask,this.visible=e.visible,this.castShadow=e.castShadow,this.receiveShadow=e.receiveShadow,this.frustumCulled=e.frustumCulled,this.renderOrder=e.renderOrder,this.animations=e.animations.slice(),this.userData=JSON.parse(JSON.stringify(e.userData)),t===!0)for(let n=0;n0?i.multiplyScalar(1/Math.sqrt(r)):i.set(0,0,0)}static getBarycoord(e,t,n,i,r){Kt.subVectors(i,t),un.subVectors(n,t),Ha.subVectors(e,t);let a=Kt.dot(Kt),o=Kt.dot(un),c=Kt.dot(Ha),l=un.dot(un),h=un.dot(Ha),u=a*l-o*o;if(u===0)return r.set(-2,-1,-1);let d=1/u,f=(l*c-o*h)*d,m=(a*h-o*c)*d;return r.set(1-f-m,m,f)}static containsPoint(e,t,n,i){return this.getBarycoord(e,t,n,i,dn),dn.x>=0&&dn.y>=0&&dn.x+dn.y<=1}static getUV(e,t,n,i,r,a,o,c){return $s===!1&&(console.warn("THREE.Triangle.getUV() has been renamed to THREE.Triangle.getInterpolation()."),$s=!0),this.getInterpolation(e,t,n,i,r,a,o,c)}static getInterpolation(e,t,n,i,r,a,o,c){return this.getBarycoord(e,t,n,i,dn),c.setScalar(0),c.addScaledVector(r,dn.x),c.addScaledVector(a,dn.y),c.addScaledVector(o,dn.z),c}static isFrontFacing(e,t,n,i){return Kt.subVectors(n,t),un.subVectors(e,t),Kt.cross(un).dot(i)<0}set(e,t,n){return this.a.copy(e),this.b.copy(t),this.c.copy(n),this}setFromPointsAndIndices(e,t,n,i){return this.a.copy(e[t]),this.b.copy(e[n]),this.c.copy(e[i]),this}setFromAttributeAndIndices(e,t,n,i){return this.a.fromBufferAttribute(e,t),this.b.fromBufferAttribute(e,n),this.c.fromBufferAttribute(e,i),this}clone(){return new this.constructor().copy(this)}copy(e){return this.a.copy(e.a),this.b.copy(e.b),this.c.copy(e.c),this}getArea(){return Kt.subVectors(this.c,this.b),un.subVectors(this.a,this.b),Kt.cross(un).length()*.5}getMidpoint(e){return e.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(e){return s.getNormal(this.a,this.b,this.c,e)}getPlane(e){return e.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(e,t){return s.getBarycoord(e,this.a,this.b,this.c,t)}getUV(e,t,n,i,r){return $s===!1&&(console.warn("THREE.Triangle.getUV() has been renamed to THREE.Triangle.getInterpolation()."),$s=!0),s.getInterpolation(e,this.a,this.b,this.c,t,n,i,r)}getInterpolation(e,t,n,i,r){return s.getInterpolation(e,this.a,this.b,this.c,t,n,i,r)}containsPoint(e){return s.containsPoint(e,this.a,this.b,this.c)}isFrontFacing(e){return s.isFrontFacing(this.a,this.b,this.c,e)}intersectsBox(e){return e.intersectsTriangle(this)}closestPointToPoint(e,t){let n=this.a,i=this.b,r=this.c,a,o;bi.subVectors(i,n),Ei.subVectors(r,n),Ga.subVectors(e,n);let c=bi.dot(Ga),l=Ei.dot(Ga);if(c<=0&&l<=0)return t.copy(n);Wa.subVectors(e,i);let h=bi.dot(Wa),u=Ei.dot(Wa);if(h>=0&&u<=h)return t.copy(i);let d=c*u-h*l;if(d<=0&&c>=0&&h<=0)return a=c/(c-h),t.copy(n).addScaledVector(bi,a);Xa.subVectors(e,r);let f=bi.dot(Xa),m=Ei.dot(Xa);if(m>=0&&f<=m)return t.copy(r);let _=f*l-c*m;if(_<=0&&l>=0&&m<=0)return o=l/(l-m),t.copy(n).addScaledVector(Ei,o);let g=h*m-f*u;if(g<=0&&u-h>=0&&f-m>=0)return Kl.subVectors(r,i),o=(u-h)/(u-h+(f-m)),t.copy(i).addScaledVector(Kl,o);let p=1/(g+_+d);return a=_*p,o=d*p,t.copy(n).addScaledVector(bi,a).addScaledVector(Ei,o)}equals(e){return e.a.equals(this.a)&&e.b.equals(this.b)&&e.c.equals(this.c)}},fp=0,bt=class extends sn{constructor(){super(),this.isMaterial=!0,Object.defineProperty(this,"id",{value:fp++}),this.uuid=kt(),this.name="",this.type="Material",this.blending=Wi,this.side=Bn,this.vertexColors=!1,this.opacity=1,this.transparent=!1,this.alphaHash=!1,this.blendSrc=ld,this.blendDst=hd,this.blendEquation=Bi,this.blendSrcAlpha=null,this.blendDstAlpha=null,this.blendEquationAlpha=null,this.depthFunc=uo,this.depthTest=!0,this.depthWrite=!0,this.stencilWriteMask=255,this.stencilFunc=If,this.stencilRef=0,this.stencilFuncMask=255,this.stencilFail=Ia,this.stencilZFail=Ia,this.stencilZPass=Ia,this.stencilWrite=!1,this.clippingPlanes=null,this.clipIntersection=!1,this.clipShadows=!1,this.shadowSide=null,this.colorWrite=!0,this.precision=null,this.polygonOffset=!1,this.polygonOffsetFactor=0,this.polygonOffsetUnits=0,this.dithering=!1,this.alphaToCoverage=!1,this.premultipliedAlpha=!1,this.forceSinglePass=!1,this.visible=!0,this.toneMapped=!0,this.userData={},this.version=0,this._alphaTest=0}get alphaTest(){return this._alphaTest}set alphaTest(e){this._alphaTest>0!=e>0&&this.version++,this._alphaTest=e}onBuild(){}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(e){if(e!==void 0)for(let t in e){let n=e[t];if(n===void 0){console.warn(`THREE.Material: parameter '${t}' has value of undefined.`);continue}let i=this[t];if(i===void 0){console.warn(`THREE.Material: '${t}' is not a property of THREE.${this.type}.`);continue}i&&i.isColor?i.set(n):i&&i.isVector3&&n&&n.isVector3?i.copy(n):this[t]=n}}toJSON(e){let t=e===void 0||typeof e=="string";t&&(e={textures:{},images:{}});let n={metadata:{version:4.6,type:"Material",generator:"Material.toJSON"}};n.uuid=this.uuid,n.type=this.type,this.name!==""&&(n.name=this.name),this.color&&this.color.isColor&&(n.color=this.color.getHex()),this.roughness!==void 0&&(n.roughness=this.roughness),this.metalness!==void 0&&(n.metalness=this.metalness),this.sheen!==void 0&&(n.sheen=this.sheen),this.sheenColor&&this.sheenColor.isColor&&(n.sheenColor=this.sheenColor.getHex()),this.sheenRoughness!==void 0&&(n.sheenRoughness=this.sheenRoughness),this.emissive&&this.emissive.isColor&&(n.emissive=this.emissive.getHex()),this.emissiveIntensity&&this.emissiveIntensity!==1&&(n.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(n.specular=this.specular.getHex()),this.specularIntensity!==void 0&&(n.specularIntensity=this.specularIntensity),this.specularColor&&this.specularColor.isColor&&(n.specularColor=this.specularColor.getHex()),this.shininess!==void 0&&(n.shininess=this.shininess),this.clearcoat!==void 0&&(n.clearcoat=this.clearcoat),this.clearcoatRoughness!==void 0&&(n.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(n.clearcoatMap=this.clearcoatMap.toJSON(e).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(n.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(e).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(n.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(e).uuid,n.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),this.iridescence!==void 0&&(n.iridescence=this.iridescence),this.iridescenceIOR!==void 0&&(n.iridescenceIOR=this.iridescenceIOR),this.iridescenceThicknessRange!==void 0&&(n.iridescenceThicknessRange=this.iridescenceThicknessRange),this.iridescenceMap&&this.iridescenceMap.isTexture&&(n.iridescenceMap=this.iridescenceMap.toJSON(e).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(n.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(e).uuid),this.anisotropy!==void 0&&(n.anisotropy=this.anisotropy),this.anisotropyRotation!==void 0&&(n.anisotropyRotation=this.anisotropyRotation),this.anisotropyMap&&this.anisotropyMap.isTexture&&(n.anisotropyMap=this.anisotropyMap.toJSON(e).uuid),this.map&&this.map.isTexture&&(n.map=this.map.toJSON(e).uuid),this.matcap&&this.matcap.isTexture&&(n.matcap=this.matcap.toJSON(e).uuid),this.alphaMap&&this.alphaMap.isTexture&&(n.alphaMap=this.alphaMap.toJSON(e).uuid),this.lightMap&&this.lightMap.isTexture&&(n.lightMap=this.lightMap.toJSON(e).uuid,n.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(n.aoMap=this.aoMap.toJSON(e).uuid,n.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(n.bumpMap=this.bumpMap.toJSON(e).uuid,n.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(n.normalMap=this.normalMap.toJSON(e).uuid,n.normalMapType=this.normalMapType,n.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(n.displacementMap=this.displacementMap.toJSON(e).uuid,n.displacementScale=this.displacementScale,n.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(n.roughnessMap=this.roughnessMap.toJSON(e).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(n.metalnessMap=this.metalnessMap.toJSON(e).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(n.emissiveMap=this.emissiveMap.toJSON(e).uuid),this.specularMap&&this.specularMap.isTexture&&(n.specularMap=this.specularMap.toJSON(e).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(n.specularIntensityMap=this.specularIntensityMap.toJSON(e).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(n.specularColorMap=this.specularColorMap.toJSON(e).uuid),this.envMap&&this.envMap.isTexture&&(n.envMap=this.envMap.toJSON(e).uuid,this.combine!==void 0&&(n.combine=this.combine)),this.envMapIntensity!==void 0&&(n.envMapIntensity=this.envMapIntensity),this.reflectivity!==void 0&&(n.reflectivity=this.reflectivity),this.refractionRatio!==void 0&&(n.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(n.gradientMap=this.gradientMap.toJSON(e).uuid),this.transmission!==void 0&&(n.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(n.transmissionMap=this.transmissionMap.toJSON(e).uuid),this.thickness!==void 0&&(n.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(n.thicknessMap=this.thicknessMap.toJSON(e).uuid),this.attenuationDistance!==void 0&&this.attenuationDistance!==1/0&&(n.attenuationDistance=this.attenuationDistance),this.attenuationColor!==void 0&&(n.attenuationColor=this.attenuationColor.getHex()),this.size!==void 0&&(n.size=this.size),this.shadowSide!==null&&(n.shadowSide=this.shadowSide),this.sizeAttenuation!==void 0&&(n.sizeAttenuation=this.sizeAttenuation),this.blending!==Wi&&(n.blending=this.blending),this.side!==Bn&&(n.side=this.side),this.vertexColors===!0&&(n.vertexColors=!0),this.opacity<1&&(n.opacity=this.opacity),this.transparent===!0&&(n.transparent=!0),n.depthFunc=this.depthFunc,n.depthTest=this.depthTest,n.depthWrite=this.depthWrite,n.colorWrite=this.colorWrite,n.stencilWrite=this.stencilWrite,n.stencilWriteMask=this.stencilWriteMask,n.stencilFunc=this.stencilFunc,n.stencilRef=this.stencilRef,n.stencilFuncMask=this.stencilFuncMask,n.stencilFail=this.stencilFail,n.stencilZFail=this.stencilZFail,n.stencilZPass=this.stencilZPass,this.rotation!==void 0&&this.rotation!==0&&(n.rotation=this.rotation),this.polygonOffset===!0&&(n.polygonOffset=!0),this.polygonOffsetFactor!==0&&(n.polygonOffsetFactor=this.polygonOffsetFactor),this.polygonOffsetUnits!==0&&(n.polygonOffsetUnits=this.polygonOffsetUnits),this.linewidth!==void 0&&this.linewidth!==1&&(n.linewidth=this.linewidth),this.dashSize!==void 0&&(n.dashSize=this.dashSize),this.gapSize!==void 0&&(n.gapSize=this.gapSize),this.scale!==void 0&&(n.scale=this.scale),this.dithering===!0&&(n.dithering=!0),this.alphaTest>0&&(n.alphaTest=this.alphaTest),this.alphaHash===!0&&(n.alphaHash=!0),this.alphaToCoverage===!0&&(n.alphaToCoverage=!0),this.premultipliedAlpha===!0&&(n.premultipliedAlpha=!0),this.forceSinglePass===!0&&(n.forceSinglePass=!0),this.wireframe===!0&&(n.wireframe=!0),this.wireframeLinewidth>1&&(n.wireframeLinewidth=this.wireframeLinewidth),this.wireframeLinecap!=="round"&&(n.wireframeLinecap=this.wireframeLinecap),this.wireframeLinejoin!=="round"&&(n.wireframeLinejoin=this.wireframeLinejoin),this.flatShading===!0&&(n.flatShading=!0),this.visible===!1&&(n.visible=!1),this.toneMapped===!1&&(n.toneMapped=!1),this.fog===!1&&(n.fog=!1),Object.keys(this.userData).length>0&&(n.userData=this.userData);function i(r){let a=[];for(let o in r){let c=r[o];delete c.metadata,a.push(c)}return a}if(t){let r=i(e.textures),a=i(e.images);r.length>0&&(n.textures=r),a.length>0&&(n.images=a)}return n}clone(){return new this.constructor().copy(this)}copy(e){this.name=e.name,this.blending=e.blending,this.side=e.side,this.vertexColors=e.vertexColors,this.opacity=e.opacity,this.transparent=e.transparent,this.blendSrc=e.blendSrc,this.blendDst=e.blendDst,this.blendEquation=e.blendEquation,this.blendSrcAlpha=e.blendSrcAlpha,this.blendDstAlpha=e.blendDstAlpha,this.blendEquationAlpha=e.blendEquationAlpha,this.depthFunc=e.depthFunc,this.depthTest=e.depthTest,this.depthWrite=e.depthWrite,this.stencilWriteMask=e.stencilWriteMask,this.stencilFunc=e.stencilFunc,this.stencilRef=e.stencilRef,this.stencilFuncMask=e.stencilFuncMask,this.stencilFail=e.stencilFail,this.stencilZFail=e.stencilZFail,this.stencilZPass=e.stencilZPass,this.stencilWrite=e.stencilWrite;let t=e.clippingPlanes,n=null;if(t!==null){let i=t.length;n=new Array(i);for(let r=0;r!==i;++r)n[r]=t[r].clone()}return this.clippingPlanes=n,this.clipIntersection=e.clipIntersection,this.clipShadows=e.clipShadows,this.shadowSide=e.shadowSide,this.colorWrite=e.colorWrite,this.precision=e.precision,this.polygonOffset=e.polygonOffset,this.polygonOffsetFactor=e.polygonOffsetFactor,this.polygonOffsetUnits=e.polygonOffsetUnits,this.dithering=e.dithering,this.alphaTest=e.alphaTest,this.alphaHash=e.alphaHash,this.alphaToCoverage=e.alphaToCoverage,this.premultipliedAlpha=e.premultipliedAlpha,this.forceSinglePass=e.forceSinglePass,this.visible=e.visible,this.toneMapped=e.toneMapped,this.userData=JSON.parse(JSON.stringify(e.userData)),this}dispose(){this.dispatchEvent({type:"dispose"})}set needsUpdate(e){e===!0&&this.version++}},Sd={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},Cn={h:0,s:0,l:0},Ks={h:0,s:0,l:0};function qa(s,e,t){return t<0&&(t+=1),t>1&&(t-=1),t<1/6?s+(e-s)*6*t:t<1/2?e:t<2/3?s+(e-s)*6*(2/3-t):s}var pe=class{constructor(e,t,n){return this.isColor=!0,this.r=1,this.g=1,this.b=1,this.set(e,t,n)}set(e,t,n){if(t===void 0&&n===void 0){let i=e;i&&i.isColor?this.copy(i):typeof i=="number"?this.setHex(i):typeof i=="string"&&this.setStyle(i)}else this.setRGB(e,t,n);return this}setScalar(e){return this.r=e,this.g=e,this.b=e,this}setHex(e,t=vt){return e=Math.floor(e),this.r=(e>>16&255)/255,this.g=(e>>8&255)/255,this.b=(e&255)/255,Qe.toWorkingColorSpace(this,t),this}setRGB(e,t,n,i=Qe.workingColorSpace){return this.r=e,this.g=t,this.b=n,Qe.toWorkingColorSpace(this,i),this}setHSL(e,t,n,i=Qe.workingColorSpace){if(e=Yc(e,1),t=ct(t,0,1),n=ct(n,0,1),t===0)this.r=this.g=this.b=n;else{let r=n<=.5?n*(1+t):n+t-n*t,a=2*n-r;this.r=qa(a,r,e+1/3),this.g=qa(a,r,e),this.b=qa(a,r,e-1/3)}return Qe.toWorkingColorSpace(this,i),this}setStyle(e,t=vt){function n(r){r!==void 0&&parseFloat(r)<1&&console.warn("THREE.Color: Alpha component of "+e+" will be ignored.")}let i;if(i=/^(\w+)\(([^\)]*)\)/.exec(e)){let r,a=i[1],o=i[2];switch(a){case"rgb":case"rgba":if(r=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setRGB(Math.min(255,parseInt(r[1],10))/255,Math.min(255,parseInt(r[2],10))/255,Math.min(255,parseInt(r[3],10))/255,t);if(r=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setRGB(Math.min(100,parseInt(r[1],10))/100,Math.min(100,parseInt(r[2],10))/100,Math.min(100,parseInt(r[3],10))/100,t);break;case"hsl":case"hsla":if(r=/^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setHSL(parseFloat(r[1])/360,parseFloat(r[2])/100,parseFloat(r[3])/100,t);break;default:console.warn("THREE.Color: Unknown color model "+e)}}else if(i=/^\#([A-Fa-f\d]+)$/.exec(e)){let r=i[1],a=r.length;if(a===3)return this.setRGB(parseInt(r.charAt(0),16)/15,parseInt(r.charAt(1),16)/15,parseInt(r.charAt(2),16)/15,t);if(a===6)return this.setHex(parseInt(r,16),t);console.warn("THREE.Color: Invalid hex color "+e)}else if(e&&e.length>0)return this.setColorName(e,t);return this}setColorName(e,t=vt){let n=Sd[e.toLowerCase()];return n!==void 0?this.setHex(n,t):console.warn("THREE.Color: Unknown color "+e),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(e){return this.r=e.r,this.g=e.g,this.b=e.b,this}copySRGBToLinear(e){return this.r=Xi(e.r),this.g=Xi(e.g),this.b=Xi(e.b),this}copyLinearToSRGB(e){return this.r=Da(e.r),this.g=Da(e.g),this.b=Da(e.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(e=vt){return Qe.fromWorkingColorSpace(Tt.copy(this),e),Math.round(ct(Tt.r*255,0,255))*65536+Math.round(ct(Tt.g*255,0,255))*256+Math.round(ct(Tt.b*255,0,255))}getHexString(e=vt){return("000000"+this.getHex(e).toString(16)).slice(-6)}getHSL(e,t=Qe.workingColorSpace){Qe.fromWorkingColorSpace(Tt.copy(this),t);let n=Tt.r,i=Tt.g,r=Tt.b,a=Math.max(n,i,r),o=Math.min(n,i,r),c,l,h=(o+a)/2;if(o===a)c=0,l=0;else{let u=a-o;switch(l=h<=.5?u/(a+o):u/(2-a-o),a){case n:c=(i-r)/u+(i>-l-14,n[c|256]=1024>>-l-14|32768,i[c]=-l-1,i[c|256]=-l-1):l<=15?(n[c]=l+15<<10,n[c|256]=l+15<<10|32768,i[c]=13,i[c|256]=13):l<128?(n[c]=31744,n[c|256]=64512,i[c]=24,i[c|256]=24):(n[c]=31744,n[c|256]=64512,i[c]=13,i[c|256]=13)}let r=new Uint32Array(2048),a=new Uint32Array(64),o=new Uint32Array(64);for(let c=1;c<1024;++c){let l=c<<13,h=0;for(;!(l&8388608);)l<<=1,h-=8388608;l&=-8388609,h+=947912704,r[c]=l|h}for(let c=1024;c<2048;++c)r[c]=939524096+(c-1024<<13);for(let c=1;c<31;++c)a[c]=c<<23;a[31]=1199570944,a[32]=2147483648;for(let c=33;c<63;++c)a[c]=2147483648+(c-32<<23);a[63]=3347054592;for(let c=1;c<64;++c)c!==32&&(o[c]=1024);return{floatView:e,uint32View:t,baseTable:n,shiftTable:i,mantissaTable:r,exponentTable:a,offsetTable:o}}function Nt(s){Math.abs(s)>65504&&console.warn("THREE.DataUtils.toHalfFloat(): Value out of range."),s=ct(s,-65504,65504),_n.floatView[0]=s;let e=_n.uint32View[0],t=e>>23&511;return _n.baseTable[t]+((e&8388607)>>_n.shiftTable[t])}function xs(s){let e=s>>10;return _n.uint32View[0]=_n.mantissaTable[_n.offsetTable[e]+(s&1023)]+_n.exponentTable[e],_n.floatView[0]}var Mv={toHalfFloat:Nt,fromHalfFloat:xs},ft=new A,Qs=new Z,et=class{constructor(e,t,n=!1){if(Array.isArray(e))throw new TypeError("THREE.BufferAttribute: array should be a Typed Array.");this.isBufferAttribute=!0,this.name="",this.array=e,this.itemSize=t,this.count=e!==void 0?e.length/t:0,this.normalized=n,this.usage=Hr,this.updateRange={offset:0,count:-1},this.gpuType=xn,this.version=0}onUploadCallback(){}set needsUpdate(e){e===!0&&this.version++}setUsage(e){return this.usage=e,this}copy(e){return this.name=e.name,this.array=new e.array.constructor(e.array),this.itemSize=e.itemSize,this.count=e.count,this.normalized=e.normalized,this.usage=e.usage,this.gpuType=e.gpuType,this}copyAt(e,t,n){e*=this.itemSize,n*=t.itemSize;for(let i=0,r=this.itemSize;i0&&(e.userData=this.userData),this.parameters!==void 0){let c=this.parameters;for(let l in c)c[l]!==void 0&&(e[l]=c[l]);return e}e.data={attributes:{}};let t=this.index;t!==null&&(e.data.index={type:t.array.constructor.name,array:Array.prototype.slice.call(t.array)});let n=this.attributes;for(let c in n){let l=n[c];e.data.attributes[c]=l.toJSON(e.data)}let i={},r=!1;for(let c in this.morphAttributes){let l=this.morphAttributes[c],h=[];for(let u=0,d=l.length;u0&&(i[c]=h,r=!0)}r&&(e.data.morphAttributes=i,e.data.morphTargetsRelative=this.morphTargetsRelative);let a=this.groups;a.length>0&&(e.data.groups=JSON.parse(JSON.stringify(a)));let o=this.boundingSphere;return o!==null&&(e.data.boundingSphere={center:o.center.toArray(),radius:o.radius}),e}clone(){return new this.constructor().copy(this)}copy(e){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;let t={};this.name=e.name;let n=e.index;n!==null&&this.setIndex(n.clone(t));let i=e.attributes;for(let l in i){let h=i[l];this.setAttribute(l,h.clone(t))}let r=e.morphAttributes;for(let l in r){let h=[],u=r[l];for(let d=0,f=u.length;d0){let i=t[n[0]];if(i!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=i.length;r(e.far-e.near)**2))&&(rh.copy(r).invert(),qn.copy(e.ray).applyMatrix4(rh),!(n.boundingBox!==null&&qn.intersectsBox(n.boundingBox)===!1)&&this._computeIntersections(e,t,qn)))}_computeIntersections(e,t,n){let i,r=this.geometry,a=this.material,o=r.index,c=r.attributes.position,l=r.attributes.uv,h=r.attributes.uv1,u=r.attributes.normal,d=r.groups,f=r.drawRange;if(o!==null)if(Array.isArray(a))for(let m=0,_=d.length;m<_;m++){let g=d[m],p=a[g.materialIndex],v=Math.max(g.start,f.start),x=Math.min(o.count,Math.min(g.start+g.count,f.start+f.count));for(let y=v,b=x;yt.far?null:{distance:l,point:rr.clone(),object:s}}function ar(s,e,t,n,i,r,a,o,c,l){s.getVertexPosition(o,wi),s.getVertexPosition(c,Ai),s.getVertexPosition(l,Ri);let h=gp(s,e,t,n,wi,Ai,Ri,sr);if(h){i&&(tr.fromBufferAttribute(i,o),nr.fromBufferAttribute(i,c),ir.fromBufferAttribute(i,l),h.uv=Un.getInterpolation(sr,wi,Ai,Ri,tr,nr,ir,new Z)),r&&(tr.fromBufferAttribute(r,o),nr.fromBufferAttribute(r,c),ir.fromBufferAttribute(r,l),h.uv1=Un.getInterpolation(sr,wi,Ai,Ri,tr,nr,ir,new Z),h.uv2=h.uv1),a&&(oh.fromBufferAttribute(a,o),ch.fromBufferAttribute(a,c),lh.fromBufferAttribute(a,l),h.normal=Un.getInterpolation(sr,wi,Ai,Ri,oh,ch,lh,new A),h.normal.dot(n.direction)>0&&h.normal.multiplyScalar(-1));let u={a:o,b:c,c:l,normal:new A,materialIndex:0};Un.getNormal(wi,Ai,Ri,u.normal),h.face=u}return h}var Ji=class s extends Ge{constructor(e=1,t=1,n=1,i=1,r=1,a=1){super(),this.type="BoxGeometry",this.parameters={width:e,height:t,depth:n,widthSegments:i,heightSegments:r,depthSegments:a};let o=this;i=Math.floor(i),r=Math.floor(r),a=Math.floor(a);let c=[],l=[],h=[],u=[],d=0,f=0;m("z","y","x",-1,-1,n,t,e,a,r,0),m("z","y","x",1,-1,n,t,-e,a,r,1),m("x","z","y",1,1,e,n,t,i,a,2),m("x","z","y",1,-1,e,n,-t,i,a,3),m("x","y","z",1,-1,e,t,n,i,r,4),m("x","y","z",-1,-1,e,t,-n,i,r,5),this.setIndex(c),this.setAttribute("position",new ve(l,3)),this.setAttribute("normal",new ve(h,3)),this.setAttribute("uv",new ve(u,2));function m(_,g,p,v,x,y,b,w,R,I,M){let T=y/R,O=b/I,Y=y/2,$=b/2,U=w/2,z=R+1,q=I+1,H=0,ne=0,W=new A;for(let K=0;K0?1:-1,h.push(W.x,W.y,W.z),u.push(G/R),u.push(1-K/I),H+=1}}for(let K=0;K0&&(t.defines=this.defines),t.vertexShader=this.vertexShader,t.fragmentShader=this.fragmentShader,t.lights=this.lights,t.clipping=this.clipping;let n={};for(let i in this.extensions)this.extensions[i]===!0&&(n[i]=!0);return Object.keys(n).length>0&&(t.extensions=n),t}},Cs=class extends Je{constructor(){super(),this.isCamera=!0,this.type="Camera",this.matrixWorldInverse=new ze,this.projectionMatrix=new ze,this.projectionMatrixInverse=new ze,this.coordinateSystem=vn}copy(e,t){return super.copy(e,t),this.matrixWorldInverse.copy(e.matrixWorldInverse),this.projectionMatrix.copy(e.projectionMatrix),this.projectionMatrixInverse.copy(e.projectionMatrixInverse),this.coordinateSystem=e.coordinateSystem,this}getWorldDirection(e){return super.getWorldDirection(e).negate()}updateMatrixWorld(e){super.updateMatrixWorld(e),this.matrixWorldInverse.copy(this.matrixWorld).invert()}updateWorldMatrix(e,t){super.updateWorldMatrix(e,t),this.matrixWorldInverse.copy(this.matrixWorld).invert()}clone(){return new this.constructor().copy(this)}},yt=class extends Cs{constructor(e=50,t=1,n=.1,i=2e3){super(),this.isPerspectiveCamera=!0,this.type="PerspectiveCamera",this.fov=e,this.zoom=1,this.near=n,this.far=i,this.focus=10,this.aspect=t,this.view=null,this.filmGauge=35,this.filmOffset=0,this.updateProjectionMatrix()}copy(e,t){return super.copy(e,t),this.fov=e.fov,this.zoom=e.zoom,this.near=e.near,this.far=e.far,this.focus=e.focus,this.aspect=e.aspect,this.view=e.view===null?null:Object.assign({},e.view),this.filmGauge=e.filmGauge,this.filmOffset=e.filmOffset,this}setFocalLength(e){let t=.5*this.getFilmHeight()/e;this.fov=Zi*2*Math.atan(t),this.updateProjectionMatrix()}getFocalLength(){let e=Math.tan(ai*.5*this.fov);return .5*this.getFilmHeight()/e}getEffectiveFOV(){return Zi*2*Math.atan(Math.tan(ai*.5*this.fov)/this.zoom)}getFilmWidth(){return this.filmGauge*Math.min(this.aspect,1)}getFilmHeight(){return this.filmGauge/Math.max(this.aspect,1)}setViewOffset(e,t,n,i,r,a){this.aspect=e/t,this.view===null&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=e,this.view.fullHeight=t,this.view.offsetX=n,this.view.offsetY=i,this.view.width=r,this.view.height=a,this.updateProjectionMatrix()}clearViewOffset(){this.view!==null&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){let e=this.near,t=e*Math.tan(ai*.5*this.fov)/this.zoom,n=2*t,i=this.aspect*n,r=-.5*i,a=this.view;if(this.view!==null&&this.view.enabled){let c=a.fullWidth,l=a.fullHeight;r+=a.offsetX*i/c,t-=a.offsetY*n/l,i*=a.width/c,n*=a.height/l}let o=this.filmOffset;o!==0&&(r+=e*o/this.getFilmWidth()),this.projectionMatrix.makePerspective(r,r+i,t,t-n,e,this.far,this.coordinateSystem),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(e){let t=super.toJSON(e);return t.object.fov=this.fov,t.object.zoom=this.zoom,t.object.near=this.near,t.object.far=this.far,t.object.focus=this.focus,t.object.aspect=this.aspect,this.view!==null&&(t.object.view=Object.assign({},this.view)),t.object.filmGauge=this.filmGauge,t.object.filmOffset=this.filmOffset,t}},Ci=-90,Pi=1,_o=class extends Je{constructor(e,t,n){super(),this.type="CubeCamera",this.renderTarget=n,this.coordinateSystem=null,this.activeMipmapLevel=0;let i=new yt(Ci,Pi,e,t);i.layers=this.layers,this.add(i);let r=new yt(Ci,Pi,e,t);r.layers=this.layers,this.add(r);let a=new yt(Ci,Pi,e,t);a.layers=this.layers,this.add(a);let o=new yt(Ci,Pi,e,t);o.layers=this.layers,this.add(o);let c=new yt(Ci,Pi,e,t);c.layers=this.layers,this.add(c);let l=new yt(Ci,Pi,e,t);l.layers=this.layers,this.add(l)}updateCoordinateSystem(){let e=this.coordinateSystem,t=this.children.concat(),[n,i,r,a,o,c]=t;for(let l of t)this.remove(l);if(e===vn)n.up.set(0,1,0),n.lookAt(1,0,0),i.up.set(0,1,0),i.lookAt(-1,0,0),r.up.set(0,0,-1),r.lookAt(0,1,0),a.up.set(0,0,1),a.lookAt(0,-1,0),o.up.set(0,1,0),o.lookAt(0,0,1),c.up.set(0,1,0),c.lookAt(0,0,-1);else if(e===Gr)n.up.set(0,-1,0),n.lookAt(-1,0,0),i.up.set(0,-1,0),i.lookAt(1,0,0),r.up.set(0,0,1),r.lookAt(0,1,0),a.up.set(0,0,-1),a.lookAt(0,-1,0),o.up.set(0,-1,0),o.lookAt(0,0,1),c.up.set(0,-1,0),c.lookAt(0,0,-1);else throw new Error("THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: "+e);for(let l of t)this.add(l),l.updateMatrixWorld()}update(e,t){this.parent===null&&this.updateMatrixWorld();let{renderTarget:n,activeMipmapLevel:i}=this;this.coordinateSystem!==e.coordinateSystem&&(this.coordinateSystem=e.coordinateSystem,this.updateCoordinateSystem());let[r,a,o,c,l,h]=this.children,u=e.getRenderTarget(),d=e.getActiveCubeFace(),f=e.getActiveMipmapLevel(),m=e.xr.enabled;e.xr.enabled=!1;let _=n.texture.generateMipmaps;n.texture.generateMipmaps=!1,e.setRenderTarget(n,0,i),e.render(t,r),e.setRenderTarget(n,1,i),e.render(t,a),e.setRenderTarget(n,2,i),e.render(t,o),e.setRenderTarget(n,3,i),e.render(t,c),e.setRenderTarget(n,4,i),e.render(t,l),n.texture.generateMipmaps=_,e.setRenderTarget(n,5,i),e.render(t,h),e.setRenderTarget(u,d,f),e.xr.enabled=m,n.texture.needsPMREMUpdate=!0}},Ki=class extends St{constructor(e,t,n,i,r,a,o,c,l,h){e=e!==void 0?e:[],t=t!==void 0?t:zn,super(e,t,n,i,r,a,o,c,l,h),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(e){this.image=e}},xo=class extends qt{constructor(e=1,t={}){super(e,e,t),this.isWebGLCubeRenderTarget=!0;let n={width:e,height:e,depth:1},i=[n,n,n,n,n,n];t.encoding!==void 0&&(Ms("THREE.WebGLCubeRenderTarget: option.encoding has been replaced by option.colorSpace."),t.colorSpace=t.encoding===ri?vt:Xt),this.texture=new Ki(i,t.mapping,t.wrapS,t.wrapT,t.magFilter,t.minFilter,t.format,t.type,t.anisotropy,t.colorSpace),this.texture.isRenderTargetTexture=!0,this.texture.generateMipmaps=t.generateMipmaps!==void 0?t.generateMipmaps:!1,this.texture.minFilter=t.minFilter!==void 0?t.minFilter:mt}fromEquirectangularTexture(e,t){this.texture.type=t.type,this.texture.colorSpace=t.colorSpace,this.texture.generateMipmaps=t.generateMipmaps,this.texture.minFilter=t.minFilter,this.texture.magFilter=t.magFilter;let n={uniforms:{tEquirect:{value:null}},vertexShader:` + + varying vec3 vWorldDirection; + + vec3 transformDirection( in vec3 dir, in mat4 matrix ) { + + return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); + + } + + void main() { + + vWorldDirection = transformDirection( position, modelMatrix ); + + #include + #include + + } + `,fragmentShader:` + + uniform sampler2D tEquirect; + + varying vec3 vWorldDirection; + + #include + + void main() { + + vec3 direction = normalize( vWorldDirection ); + + vec2 sampleUV = equirectUv( direction ); + + gl_FragColor = texture2D( tEquirect, sampleUV ); + + } + `},i=new Ji(5,5,5),r=new jt({name:"CubemapFromEquirect",uniforms:$i(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:Ft,blending:Dn});r.uniforms.tEquirect.value=t;let a=new Mt(i,r),o=t.minFilter;return t.minFilter===li&&(t.minFilter=mt),new _o(1,10,this).update(e,a),t.minFilter=o,a.geometry.dispose(),a.material.dispose(),this}clear(e,t,n,i){let r=e.getRenderTarget();for(let a=0;a<6;a++)e.setRenderTarget(this,a),e.clear(t,n,i);e.setRenderTarget(r)}},Ja=new A,Mp=new A,Sp=new He,mn=class{constructor(e=new A(1,0,0),t=0){this.isPlane=!0,this.normal=e,this.constant=t}set(e,t){return this.normal.copy(e),this.constant=t,this}setComponents(e,t,n,i){return this.normal.set(e,t,n),this.constant=i,this}setFromNormalAndCoplanarPoint(e,t){return this.normal.copy(e),this.constant=-t.dot(this.normal),this}setFromCoplanarPoints(e,t,n){let i=Ja.subVectors(n,t).cross(Mp.subVectors(e,t)).normalize();return this.setFromNormalAndCoplanarPoint(i,e),this}copy(e){return this.normal.copy(e.normal),this.constant=e.constant,this}normalize(){let e=1/this.normal.length();return this.normal.multiplyScalar(e),this.constant*=e,this}negate(){return this.constant*=-1,this.normal.negate(),this}distanceToPoint(e){return this.normal.dot(e)+this.constant}distanceToSphere(e){return this.distanceToPoint(e.center)-e.radius}projectPoint(e,t){return t.copy(e).addScaledVector(this.normal,-this.distanceToPoint(e))}intersectLine(e,t){let n=e.delta(Ja),i=this.normal.dot(n);if(i===0)return this.distanceToPoint(e.start)===0?t.copy(e.start):null;let r=-(e.start.dot(this.normal)+this.constant)/i;return r<0||r>1?null:t.copy(e.start).addScaledVector(n,r)}intersectsLine(e){let t=this.distanceToPoint(e.start),n=this.distanceToPoint(e.end);return t<0&&n>0||n<0&&t>0}intersectsBox(e){return e.intersectsPlane(this)}intersectsSphere(e){return e.intersectsPlane(this)}coplanarPoint(e){return e.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(e,t){let n=t||Sp.getNormalMatrix(e),i=this.coplanarPoint(Ja).applyMatrix4(e),r=this.normal.applyMatrix3(n).normalize();return this.constant=-i.dot(r),this}translate(e){return this.constant-=e.dot(this.normal),this}equals(e){return e.normal.equals(this.normal)&&e.constant===this.constant}clone(){return new this.constructor().copy(this)}},Yn=new Yt,or=new A,Ps=class{constructor(e=new mn,t=new mn,n=new mn,i=new mn,r=new mn,a=new mn){this.planes=[e,t,n,i,r,a]}set(e,t,n,i,r,a){let o=this.planes;return o[0].copy(e),o[1].copy(t),o[2].copy(n),o[3].copy(i),o[4].copy(r),o[5].copy(a),this}copy(e){let t=this.planes;for(let n=0;n<6;n++)t[n].copy(e.planes[n]);return this}setFromProjectionMatrix(e,t=vn){let n=this.planes,i=e.elements,r=i[0],a=i[1],o=i[2],c=i[3],l=i[4],h=i[5],u=i[6],d=i[7],f=i[8],m=i[9],_=i[10],g=i[11],p=i[12],v=i[13],x=i[14],y=i[15];if(n[0].setComponents(c-r,d-l,g-f,y-p).normalize(),n[1].setComponents(c+r,d+l,g+f,y+p).normalize(),n[2].setComponents(c+a,d+h,g+m,y+v).normalize(),n[3].setComponents(c-a,d-h,g-m,y-v).normalize(),n[4].setComponents(c-o,d-u,g-_,y-x).normalize(),t===vn)n[5].setComponents(c+o,d+u,g+_,y+x).normalize();else if(t===Gr)n[5].setComponents(o,u,_,x).normalize();else throw new Error("THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: "+t);return this}intersectsObject(e){if(e.boundingSphere!==void 0)e.boundingSphere===null&&e.computeBoundingSphere(),Yn.copy(e.boundingSphere).applyMatrix4(e.matrixWorld);else{let t=e.geometry;t.boundingSphere===null&&t.computeBoundingSphere(),Yn.copy(t.boundingSphere).applyMatrix4(e.matrixWorld)}return this.intersectsSphere(Yn)}intersectsSprite(e){return Yn.center.set(0,0,0),Yn.radius=.7071067811865476,Yn.applyMatrix4(e.matrixWorld),this.intersectsSphere(Yn)}intersectsSphere(e){let t=this.planes,n=e.center,i=-e.radius;for(let r=0;r<6;r++)if(t[r].distanceToPoint(n)0?e.max.x:e.min.x,or.y=i.normal.y>0?e.max.y:e.min.y,or.z=i.normal.z>0?e.max.z:e.min.z,i.distanceToPoint(or)<0)return!1}return!0}containsPoint(e){let t=this.planes;for(let n=0;n<6;n++)if(t[n].distanceToPoint(e)<0)return!1;return!0}clone(){return new this.constructor().copy(this)}};function Ed(){let s=null,e=!1,t=null,n=null;function i(r,a){t(r,a),n=s.requestAnimationFrame(i)}return{start:function(){e!==!0&&t!==null&&(n=s.requestAnimationFrame(i),e=!0)},stop:function(){s.cancelAnimationFrame(n),e=!1},setAnimationLoop:function(r){t=r},setContext:function(r){s=r}}}function bp(s,e){let t=e.isWebGL2,n=new WeakMap;function i(l,h){let u=l.array,d=l.usage,f=s.createBuffer();s.bindBuffer(h,f),s.bufferData(h,u,d),l.onUploadCallback();let m;if(u instanceof Float32Array)m=s.FLOAT;else if(u instanceof Uint16Array)if(l.isFloat16BufferAttribute)if(t)m=s.HALF_FLOAT;else throw new Error("THREE.WebGLAttributes: Usage of Float16BufferAttribute requires WebGL2.");else m=s.UNSIGNED_SHORT;else if(u instanceof Int16Array)m=s.SHORT;else if(u instanceof Uint32Array)m=s.UNSIGNED_INT;else if(u instanceof Int32Array)m=s.INT;else if(u instanceof Int8Array)m=s.BYTE;else if(u instanceof Uint8Array)m=s.UNSIGNED_BYTE;else if(u instanceof Uint8ClampedArray)m=s.UNSIGNED_BYTE;else throw new Error("THREE.WebGLAttributes: Unsupported buffer data format: "+u);return{buffer:f,type:m,bytesPerElement:u.BYTES_PER_ELEMENT,version:l.version}}function r(l,h,u){let d=h.array,f=h.updateRange;s.bindBuffer(u,l),f.count===-1?s.bufferSubData(u,0,d):(t?s.bufferSubData(u,f.offset*d.BYTES_PER_ELEMENT,d,f.offset,f.count):s.bufferSubData(u,f.offset*d.BYTES_PER_ELEMENT,d.subarray(f.offset,f.offset+f.count)),f.count=-1),h.onUploadCallback()}function a(l){return l.isInterleavedBufferAttribute&&(l=l.data),n.get(l)}function o(l){l.isInterleavedBufferAttribute&&(l=l.data);let h=n.get(l);h&&(s.deleteBuffer(h.buffer),n.delete(l))}function c(l,h){if(l.isGLBufferAttribute){let d=n.get(l);(!d||d.version 0 + vec4 plane; + #pragma unroll_loop_start + for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + if ( dot( vClipPosition, plane.xyz ) > plane.w ) discard; + } + #pragma unroll_loop_end + #if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES + bool clipped = true; + #pragma unroll_loop_start + for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + clipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped; + } + #pragma unroll_loop_end + if ( clipped ) discard; + #endif +#endif`,Bp=`#if NUM_CLIPPING_PLANES > 0 + varying vec3 vClipPosition; + uniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ]; +#endif`,zp=`#if NUM_CLIPPING_PLANES > 0 + varying vec3 vClipPosition; +#endif`,Vp=`#if NUM_CLIPPING_PLANES > 0 + vClipPosition = - mvPosition.xyz; +#endif`,kp=`#if defined( USE_COLOR_ALPHA ) + diffuseColor *= vColor; +#elif defined( USE_COLOR ) + diffuseColor.rgb *= vColor; +#endif`,Hp=`#if defined( USE_COLOR_ALPHA ) + varying vec4 vColor; +#elif defined( USE_COLOR ) + varying vec3 vColor; +#endif`,Gp=`#if defined( USE_COLOR_ALPHA ) + varying vec4 vColor; +#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) + varying vec3 vColor; +#endif`,Wp=`#if defined( USE_COLOR_ALPHA ) + vColor = vec4( 1.0 ); +#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) + vColor = vec3( 1.0 ); +#endif +#ifdef USE_COLOR + vColor *= color; +#endif +#ifdef USE_INSTANCING_COLOR + vColor.xyz *= instanceColor.xyz; +#endif`,Xp=`#define PI 3.141592653589793 +#define PI2 6.283185307179586 +#define PI_HALF 1.5707963267948966 +#define RECIPROCAL_PI 0.3183098861837907 +#define RECIPROCAL_PI2 0.15915494309189535 +#define EPSILON 1e-6 +#ifndef saturate +#define saturate( a ) clamp( a, 0.0, 1.0 ) +#endif +#define whiteComplement( a ) ( 1.0 - saturate( a ) ) +float pow2( const in float x ) { return x*x; } +vec3 pow2( const in vec3 x ) { return x*x; } +float pow3( const in float x ) { return x*x*x; } +float pow4( const in float x ) { float x2 = x*x; return x2*x2; } +float max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); } +float average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); } +highp float rand( const in vec2 uv ) { + const highp float a = 12.9898, b = 78.233, c = 43758.5453; + highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); + return fract( sin( sn ) * c ); +} +#ifdef HIGH_PRECISION + float precisionSafeLength( vec3 v ) { return length( v ); } +#else + float precisionSafeLength( vec3 v ) { + float maxComponent = max3( abs( v ) ); + return length( v / maxComponent ) * maxComponent; + } +#endif +struct IncidentLight { + vec3 color; + vec3 direction; + bool visible; +}; +struct ReflectedLight { + vec3 directDiffuse; + vec3 directSpecular; + vec3 indirectDiffuse; + vec3 indirectSpecular; +}; +#ifdef USE_ALPHAHASH + varying vec3 vPosition; +#endif +vec3 transformDirection( in vec3 dir, in mat4 matrix ) { + return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); +} +vec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) { + return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz ); +} +mat3 transposeMat3( const in mat3 m ) { + mat3 tmp; + tmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x ); + tmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y ); + tmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z ); + return tmp; +} +float luminance( const in vec3 rgb ) { + const vec3 weights = vec3( 0.2126729, 0.7151522, 0.0721750 ); + return dot( weights, rgb ); +} +bool isPerspectiveMatrix( mat4 m ) { + return m[ 2 ][ 3 ] == - 1.0; +} +vec2 equirectUv( in vec3 dir ) { + float u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5; + float v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5; + return vec2( u, v ); +} +vec3 BRDF_Lambert( const in vec3 diffuseColor ) { + return RECIPROCAL_PI * diffuseColor; +} +vec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) { + float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); + return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); +} +float F_Schlick( const in float f0, const in float f90, const in float dotVH ) { + float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); + return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); +} // validated`,qp=`#ifdef ENVMAP_TYPE_CUBE_UV + #define cubeUV_minMipLevel 4.0 + #define cubeUV_minTileSize 16.0 + float getFace( vec3 direction ) { + vec3 absDirection = abs( direction ); + float face = - 1.0; + if ( absDirection.x > absDirection.z ) { + if ( absDirection.x > absDirection.y ) + face = direction.x > 0.0 ? 0.0 : 3.0; + else + face = direction.y > 0.0 ? 1.0 : 4.0; + } else { + if ( absDirection.z > absDirection.y ) + face = direction.z > 0.0 ? 2.0 : 5.0; + else + face = direction.y > 0.0 ? 1.0 : 4.0; + } + return face; + } + vec2 getUV( vec3 direction, float face ) { + vec2 uv; + if ( face == 0.0 ) { + uv = vec2( direction.z, direction.y ) / abs( direction.x ); + } else if ( face == 1.0 ) { + uv = vec2( - direction.x, - direction.z ) / abs( direction.y ); + } else if ( face == 2.0 ) { + uv = vec2( - direction.x, direction.y ) / abs( direction.z ); + } else if ( face == 3.0 ) { + uv = vec2( - direction.z, direction.y ) / abs( direction.x ); + } else if ( face == 4.0 ) { + uv = vec2( - direction.x, direction.z ) / abs( direction.y ); + } else { + uv = vec2( direction.x, direction.y ) / abs( direction.z ); + } + return 0.5 * ( uv + 1.0 ); + } + vec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) { + float face = getFace( direction ); + float filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 ); + mipInt = max( mipInt, cubeUV_minMipLevel ); + float faceSize = exp2( mipInt ); + highp vec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0; + if ( face > 2.0 ) { + uv.y += faceSize; + face -= 3.0; + } + uv.x += face * faceSize; + uv.x += filterInt * 3.0 * cubeUV_minTileSize; + uv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize ); + uv.x *= CUBEUV_TEXEL_WIDTH; + uv.y *= CUBEUV_TEXEL_HEIGHT; + #ifdef texture2DGradEXT + return texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb; + #else + return texture2D( envMap, uv ).rgb; + #endif + } + #define cubeUV_r0 1.0 + #define cubeUV_v0 0.339 + #define cubeUV_m0 - 2.0 + #define cubeUV_r1 0.8 + #define cubeUV_v1 0.276 + #define cubeUV_m1 - 1.0 + #define cubeUV_r4 0.4 + #define cubeUV_v4 0.046 + #define cubeUV_m4 2.0 + #define cubeUV_r5 0.305 + #define cubeUV_v5 0.016 + #define cubeUV_m5 3.0 + #define cubeUV_r6 0.21 + #define cubeUV_v6 0.0038 + #define cubeUV_m6 4.0 + float roughnessToMip( float roughness ) { + float mip = 0.0; + if ( roughness >= cubeUV_r1 ) { + mip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0; + } else if ( roughness >= cubeUV_r4 ) { + mip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1; + } else if ( roughness >= cubeUV_r5 ) { + mip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4; + } else if ( roughness >= cubeUV_r6 ) { + mip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5; + } else { + mip = - 2.0 * log2( 1.16 * roughness ); } + return mip; + } + vec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) { + float mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP ); + float mipF = fract( mip ); + float mipInt = floor( mip ); + vec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt ); + if ( mipF == 0.0 ) { + return vec4( color0, 1.0 ); + } else { + vec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 ); + return vec4( mix( color0, color1, mipF ), 1.0 ); + } + } +#endif`,Yp=`vec3 transformedNormal = objectNormal; +#ifdef USE_INSTANCING + mat3 m = mat3( instanceMatrix ); + transformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) ); + transformedNormal = m * transformedNormal; +#endif +transformedNormal = normalMatrix * transformedNormal; +#ifdef FLIP_SIDED + transformedNormal = - transformedNormal; +#endif +#ifdef USE_TANGENT + vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz; + #ifdef FLIP_SIDED + transformedTangent = - transformedTangent; + #endif +#endif`,Zp=`#ifdef USE_DISPLACEMENTMAP + uniform sampler2D displacementMap; + uniform float displacementScale; + uniform float displacementBias; +#endif`,Jp=`#ifdef USE_DISPLACEMENTMAP + transformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias ); +#endif`,$p=`#ifdef USE_EMISSIVEMAP + vec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv ); + totalEmissiveRadiance *= emissiveColor.rgb; +#endif`,Kp=`#ifdef USE_EMISSIVEMAP + uniform sampler2D emissiveMap; +#endif`,Qp="gl_FragColor = linearToOutputTexel( gl_FragColor );",jp=` +const mat3 LINEAR_SRGB_TO_LINEAR_DISPLAY_P3 = mat3( + vec3( 0.8224621, 0.177538, 0.0 ), + vec3( 0.0331941, 0.9668058, 0.0 ), + vec3( 0.0170827, 0.0723974, 0.9105199 ) +); +const mat3 LINEAR_DISPLAY_P3_TO_LINEAR_SRGB = mat3( + vec3( 1.2249401, - 0.2249404, 0.0 ), + vec3( - 0.0420569, 1.0420571, 0.0 ), + vec3( - 0.0196376, - 0.0786361, 1.0982735 ) +); +vec4 LinearSRGBToLinearDisplayP3( in vec4 value ) { + return vec4( value.rgb * LINEAR_SRGB_TO_LINEAR_DISPLAY_P3, value.a ); +} +vec4 LinearDisplayP3ToLinearSRGB( in vec4 value ) { + return vec4( value.rgb * LINEAR_DISPLAY_P3_TO_LINEAR_SRGB, value.a ); +} +vec4 LinearTransferOETF( in vec4 value ) { + return value; +} +vec4 sRGBTransferOETF( in vec4 value ) { + return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a ); +} +vec4 LinearToLinear( in vec4 value ) { + return value; +} +vec4 LinearTosRGB( in vec4 value ) { + return sRGBTransferOETF( value ); +}`,em=`#ifdef USE_ENVMAP + #ifdef ENV_WORLDPOS + vec3 cameraToFrag; + if ( isOrthographic ) { + cameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); + } else { + cameraToFrag = normalize( vWorldPosition - cameraPosition ); + } + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + #ifdef ENVMAP_MODE_REFLECTION + vec3 reflectVec = reflect( cameraToFrag, worldNormal ); + #else + vec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio ); + #endif + #else + vec3 reflectVec = vReflect; + #endif + #ifdef ENVMAP_TYPE_CUBE + vec4 envColor = textureCube( envMap, vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) ); + #else + vec4 envColor = vec4( 0.0 ); + #endif + #ifdef ENVMAP_BLENDING_MULTIPLY + outgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity ); + #elif defined( ENVMAP_BLENDING_MIX ) + outgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity ); + #elif defined( ENVMAP_BLENDING_ADD ) + outgoingLight += envColor.xyz * specularStrength * reflectivity; + #endif +#endif`,tm=`#ifdef USE_ENVMAP + uniform float envMapIntensity; + uniform float flipEnvMap; + #ifdef ENVMAP_TYPE_CUBE + uniform samplerCube envMap; + #else + uniform sampler2D envMap; + #endif + +#endif`,nm=`#ifdef USE_ENVMAP + uniform float reflectivity; + #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) + #define ENV_WORLDPOS + #endif + #ifdef ENV_WORLDPOS + varying vec3 vWorldPosition; + uniform float refractionRatio; + #else + varying vec3 vReflect; + #endif +#endif`,im=`#ifdef USE_ENVMAP + #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) + #define ENV_WORLDPOS + #endif + #ifdef ENV_WORLDPOS + + varying vec3 vWorldPosition; + #else + varying vec3 vReflect; + uniform float refractionRatio; + #endif +#endif`,sm=`#ifdef USE_ENVMAP + #ifdef ENV_WORLDPOS + vWorldPosition = worldPosition.xyz; + #else + vec3 cameraToVertex; + if ( isOrthographic ) { + cameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); + } else { + cameraToVertex = normalize( worldPosition.xyz - cameraPosition ); + } + vec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); + #ifdef ENVMAP_MODE_REFLECTION + vReflect = reflect( cameraToVertex, worldNormal ); + #else + vReflect = refract( cameraToVertex, worldNormal, refractionRatio ); + #endif + #endif +#endif`,rm=`#ifdef USE_FOG + vFogDepth = - mvPosition.z; +#endif`,am=`#ifdef USE_FOG + varying float vFogDepth; +#endif`,om=`#ifdef USE_FOG + #ifdef FOG_EXP2 + float fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth ); + #else + float fogFactor = smoothstep( fogNear, fogFar, vFogDepth ); + #endif + gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor ); +#endif`,cm=`#ifdef USE_FOG + uniform vec3 fogColor; + varying float vFogDepth; + #ifdef FOG_EXP2 + uniform float fogDensity; + #else + uniform float fogNear; + uniform float fogFar; + #endif +#endif`,lm=`#ifdef USE_GRADIENTMAP + uniform sampler2D gradientMap; +#endif +vec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) { + float dotNL = dot( normal, lightDirection ); + vec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 ); + #ifdef USE_GRADIENTMAP + return vec3( texture2D( gradientMap, coord ).r ); + #else + vec2 fw = fwidth( coord ) * 0.5; + return mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) ); + #endif +}`,hm=`#ifdef USE_LIGHTMAP + vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); + vec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity; + reflectedLight.indirectDiffuse += lightMapIrradiance; +#endif`,um=`#ifdef USE_LIGHTMAP + uniform sampler2D lightMap; + uniform float lightMapIntensity; +#endif`,dm=`LambertMaterial material; +material.diffuseColor = diffuseColor.rgb; +material.specularStrength = specularStrength;`,fm=`varying vec3 vViewPosition; +struct LambertMaterial { + vec3 diffuseColor; + float specularStrength; +}; +void RE_Direct_Lambert( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_Lambert +#define RE_IndirectDiffuse RE_IndirectDiffuse_Lambert`,pm=`uniform bool receiveShadow; +uniform vec3 ambientLightColor; +#if defined( USE_LIGHT_PROBES ) + uniform vec3 lightProbe[ 9 ]; +#endif +vec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) { + float x = normal.x, y = normal.y, z = normal.z; + vec3 result = shCoefficients[ 0 ] * 0.886227; + result += shCoefficients[ 1 ] * 2.0 * 0.511664 * y; + result += shCoefficients[ 2 ] * 2.0 * 0.511664 * z; + result += shCoefficients[ 3 ] * 2.0 * 0.511664 * x; + result += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y; + result += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z; + result += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 ); + result += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z; + result += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y ); + return result; +} +vec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) { + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + vec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe ); + return irradiance; +} +vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) { + vec3 irradiance = ambientLightColor; + return irradiance; +} +float getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) { + #if defined ( LEGACY_LIGHTS ) + if ( cutoffDistance > 0.0 && decayExponent > 0.0 ) { + return pow( saturate( - lightDistance / cutoffDistance + 1.0 ), decayExponent ); + } + return 1.0; + #else + float distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 ); + if ( cutoffDistance > 0.0 ) { + distanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) ); + } + return distanceFalloff; + #endif +} +float getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) { + return smoothstep( coneCosine, penumbraCosine, angleCosine ); +} +#if NUM_DIR_LIGHTS > 0 + struct DirectionalLight { + vec3 direction; + vec3 color; + }; + uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ]; + void getDirectionalLightInfo( const in DirectionalLight directionalLight, out IncidentLight light ) { + light.color = directionalLight.color; + light.direction = directionalLight.direction; + light.visible = true; + } +#endif +#if NUM_POINT_LIGHTS > 0 + struct PointLight { + vec3 position; + vec3 color; + float distance; + float decay; + }; + uniform PointLight pointLights[ NUM_POINT_LIGHTS ]; + void getPointLightInfo( const in PointLight pointLight, const in vec3 geometryPosition, out IncidentLight light ) { + vec3 lVector = pointLight.position - geometryPosition; + light.direction = normalize( lVector ); + float lightDistance = length( lVector ); + light.color = pointLight.color; + light.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay ); + light.visible = ( light.color != vec3( 0.0 ) ); + } +#endif +#if NUM_SPOT_LIGHTS > 0 + struct SpotLight { + vec3 position; + vec3 direction; + vec3 color; + float distance; + float decay; + float coneCos; + float penumbraCos; + }; + uniform SpotLight spotLights[ NUM_SPOT_LIGHTS ]; + void getSpotLightInfo( const in SpotLight spotLight, const in vec3 geometryPosition, out IncidentLight light ) { + vec3 lVector = spotLight.position - geometryPosition; + light.direction = normalize( lVector ); + float angleCos = dot( light.direction, spotLight.direction ); + float spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos ); + if ( spotAttenuation > 0.0 ) { + float lightDistance = length( lVector ); + light.color = spotLight.color * spotAttenuation; + light.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay ); + light.visible = ( light.color != vec3( 0.0 ) ); + } else { + light.color = vec3( 0.0 ); + light.visible = false; + } + } +#endif +#if NUM_RECT_AREA_LIGHTS > 0 + struct RectAreaLight { + vec3 color; + vec3 position; + vec3 halfWidth; + vec3 halfHeight; + }; + uniform sampler2D ltc_1; uniform sampler2D ltc_2; + uniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ]; +#endif +#if NUM_HEMI_LIGHTS > 0 + struct HemisphereLight { + vec3 direction; + vec3 skyColor; + vec3 groundColor; + }; + uniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ]; + vec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) { + float dotNL = dot( normal, hemiLight.direction ); + float hemiDiffuseWeight = 0.5 * dotNL + 0.5; + vec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight ); + return irradiance; + } +#endif`,mm=`#ifdef USE_ENVMAP + vec3 getIBLIrradiance( const in vec3 normal ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + vec4 envMapColor = textureCubeUV( envMap, worldNormal, 1.0 ); + return PI * envMapColor.rgb * envMapIntensity; + #else + return vec3( 0.0 ); + #endif + } + vec3 getIBLRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 reflectVec = reflect( - viewDir, normal ); + reflectVec = normalize( mix( reflectVec, normal, roughness * roughness) ); + reflectVec = inverseTransformDirection( reflectVec, viewMatrix ); + vec4 envMapColor = textureCubeUV( envMap, reflectVec, roughness ); + return envMapColor.rgb * envMapIntensity; + #else + return vec3( 0.0 ); + #endif + } + #ifdef USE_ANISOTROPY + vec3 getIBLAnisotropyRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in vec3 bitangent, const in float anisotropy ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 bentNormal = cross( bitangent, viewDir ); + bentNormal = normalize( cross( bentNormal, bitangent ) ); + bentNormal = normalize( mix( bentNormal, normal, pow2( pow2( 1.0 - anisotropy * ( 1.0 - roughness ) ) ) ) ); + return getIBLRadiance( viewDir, bentNormal, roughness ); + #else + return vec3( 0.0 ); + #endif + } + #endif +#endif`,gm=`ToonMaterial material; +material.diffuseColor = diffuseColor.rgb;`,_m=`varying vec3 vViewPosition; +struct ToonMaterial { + vec3 diffuseColor; +}; +void RE_Direct_Toon( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { + vec3 irradiance = getGradientIrradiance( geometryNormal, directLight.direction ) * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_Toon +#define RE_IndirectDiffuse RE_IndirectDiffuse_Toon`,xm=`BlinnPhongMaterial material; +material.diffuseColor = diffuseColor.rgb; +material.specularColor = specular; +material.specularShininess = shininess; +material.specularStrength = specularStrength;`,vm=`varying vec3 vViewPosition; +struct BlinnPhongMaterial { + vec3 diffuseColor; + vec3 specularColor; + float specularShininess; + float specularStrength; +}; +void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); + reflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometryViewDir, geometryNormal, material.specularColor, material.specularShininess ) * material.specularStrength; +} +void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_BlinnPhong +#define RE_IndirectDiffuse RE_IndirectDiffuse_BlinnPhong`,ym=`PhysicalMaterial material; +material.diffuseColor = diffuseColor.rgb * ( 1.0 - metalnessFactor ); +vec3 dxy = max( abs( dFdx( nonPerturbedNormal ) ), abs( dFdy( nonPerturbedNormal ) ) ); +float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z ); +material.roughness = max( roughnessFactor, 0.0525 );material.roughness += geometryRoughness; +material.roughness = min( material.roughness, 1.0 ); +#ifdef IOR + material.ior = ior; + #ifdef USE_SPECULAR + float specularIntensityFactor = specularIntensity; + vec3 specularColorFactor = specularColor; + #ifdef USE_SPECULAR_COLORMAP + specularColorFactor *= texture2D( specularColorMap, vSpecularColorMapUv ).rgb; + #endif + #ifdef USE_SPECULAR_INTENSITYMAP + specularIntensityFactor *= texture2D( specularIntensityMap, vSpecularIntensityMapUv ).a; + #endif + material.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor ); + #else + float specularIntensityFactor = 1.0; + vec3 specularColorFactor = vec3( 1.0 ); + material.specularF90 = 1.0; + #endif + material.specularColor = mix( min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor, diffuseColor.rgb, metalnessFactor ); +#else + material.specularColor = mix( vec3( 0.04 ), diffuseColor.rgb, metalnessFactor ); + material.specularF90 = 1.0; +#endif +#ifdef USE_CLEARCOAT + material.clearcoat = clearcoat; + material.clearcoatRoughness = clearcoatRoughness; + material.clearcoatF0 = vec3( 0.04 ); + material.clearcoatF90 = 1.0; + #ifdef USE_CLEARCOATMAP + material.clearcoat *= texture2D( clearcoatMap, vClearcoatMapUv ).x; + #endif + #ifdef USE_CLEARCOAT_ROUGHNESSMAP + material.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vClearcoatRoughnessMapUv ).y; + #endif + material.clearcoat = saturate( material.clearcoat ); material.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 ); + material.clearcoatRoughness += geometryRoughness; + material.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 ); +#endif +#ifdef USE_IRIDESCENCE + material.iridescence = iridescence; + material.iridescenceIOR = iridescenceIOR; + #ifdef USE_IRIDESCENCEMAP + material.iridescence *= texture2D( iridescenceMap, vIridescenceMapUv ).r; + #endif + #ifdef USE_IRIDESCENCE_THICKNESSMAP + material.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vIridescenceThicknessMapUv ).g + iridescenceThicknessMinimum; + #else + material.iridescenceThickness = iridescenceThicknessMaximum; + #endif +#endif +#ifdef USE_SHEEN + material.sheenColor = sheenColor; + #ifdef USE_SHEEN_COLORMAP + material.sheenColor *= texture2D( sheenColorMap, vSheenColorMapUv ).rgb; + #endif + material.sheenRoughness = clamp( sheenRoughness, 0.07, 1.0 ); + #ifdef USE_SHEEN_ROUGHNESSMAP + material.sheenRoughness *= texture2D( sheenRoughnessMap, vSheenRoughnessMapUv ).a; + #endif +#endif +#ifdef USE_ANISOTROPY + #ifdef USE_ANISOTROPYMAP + mat2 anisotropyMat = mat2( anisotropyVector.x, anisotropyVector.y, - anisotropyVector.y, anisotropyVector.x ); + vec3 anisotropyPolar = texture2D( anisotropyMap, vAnisotropyMapUv ).rgb; + vec2 anisotropyV = anisotropyMat * normalize( 2.0 * anisotropyPolar.rg - vec2( 1.0 ) ) * anisotropyPolar.b; + #else + vec2 anisotropyV = anisotropyVector; + #endif + material.anisotropy = length( anisotropyV ); + anisotropyV /= material.anisotropy; + material.anisotropy = saturate( material.anisotropy ); + material.alphaT = mix( pow2( material.roughness ), 1.0, pow2( material.anisotropy ) ); + material.anisotropyT = tbn[ 0 ] * anisotropyV.x - tbn[ 1 ] * anisotropyV.y; + material.anisotropyB = tbn[ 1 ] * anisotropyV.x + tbn[ 0 ] * anisotropyV.y; +#endif`,Mm=`struct PhysicalMaterial { + vec3 diffuseColor; + float roughness; + vec3 specularColor; + float specularF90; + #ifdef USE_CLEARCOAT + float clearcoat; + float clearcoatRoughness; + vec3 clearcoatF0; + float clearcoatF90; + #endif + #ifdef USE_IRIDESCENCE + float iridescence; + float iridescenceIOR; + float iridescenceThickness; + vec3 iridescenceFresnel; + vec3 iridescenceF0; + #endif + #ifdef USE_SHEEN + vec3 sheenColor; + float sheenRoughness; + #endif + #ifdef IOR + float ior; + #endif + #ifdef USE_TRANSMISSION + float transmission; + float transmissionAlpha; + float thickness; + float attenuationDistance; + vec3 attenuationColor; + #endif + #ifdef USE_ANISOTROPY + float anisotropy; + float alphaT; + vec3 anisotropyT; + vec3 anisotropyB; + #endif +}; +vec3 clearcoatSpecular = vec3( 0.0 ); +vec3 sheenSpecular = vec3( 0.0 ); +vec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) { + float x = clamp( 1.0 - dotVH, 0.0, 1.0 ); + float x2 = x * x; + float x5 = clamp( x * x2 * x2, 0.0, 0.9999 ); + return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 ); +} +float V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) { + float a2 = pow2( alpha ); + float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) ); + float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) ); + return 0.5 / max( gv + gl, EPSILON ); +} +float D_GGX( const in float alpha, const in float dotNH ) { + float a2 = pow2( alpha ); + float denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0; + return RECIPROCAL_PI * a2 / pow2( denom ); +} +#ifdef USE_ANISOTROPY + float V_GGX_SmithCorrelated_Anisotropic( const in float alphaT, const in float alphaB, const in float dotTV, const in float dotBV, const in float dotTL, const in float dotBL, const in float dotNV, const in float dotNL ) { + float gv = dotNL * length( vec3( alphaT * dotTV, alphaB * dotBV, dotNV ) ); + float gl = dotNV * length( vec3( alphaT * dotTL, alphaB * dotBL, dotNL ) ); + float v = 0.5 / ( gv + gl ); + return saturate(v); + } + float D_GGX_Anisotropic( const in float alphaT, const in float alphaB, const in float dotNH, const in float dotTH, const in float dotBH ) { + float a2 = alphaT * alphaB; + highp vec3 v = vec3( alphaB * dotTH, alphaT * dotBH, a2 * dotNH ); + highp float v2 = dot( v, v ); + float w2 = a2 / v2; + return RECIPROCAL_PI * a2 * pow2 ( w2 ); + } +#endif +#ifdef USE_CLEARCOAT + vec3 BRDF_GGX_Clearcoat( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material) { + vec3 f0 = material.clearcoatF0; + float f90 = material.clearcoatF90; + float roughness = material.clearcoatRoughness; + float alpha = pow2( roughness ); + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float dotVH = saturate( dot( viewDir, halfDir ) ); + vec3 F = F_Schlick( f0, f90, dotVH ); + float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); + float D = D_GGX( alpha, dotNH ); + return F * ( V * D ); + } +#endif +vec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) { + vec3 f0 = material.specularColor; + float f90 = material.specularF90; + float roughness = material.roughness; + float alpha = pow2( roughness ); + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float dotVH = saturate( dot( viewDir, halfDir ) ); + vec3 F = F_Schlick( f0, f90, dotVH ); + #ifdef USE_IRIDESCENCE + F = mix( F, material.iridescenceFresnel, material.iridescence ); + #endif + #ifdef USE_ANISOTROPY + float dotTL = dot( material.anisotropyT, lightDir ); + float dotTV = dot( material.anisotropyT, viewDir ); + float dotTH = dot( material.anisotropyT, halfDir ); + float dotBL = dot( material.anisotropyB, lightDir ); + float dotBV = dot( material.anisotropyB, viewDir ); + float dotBH = dot( material.anisotropyB, halfDir ); + float V = V_GGX_SmithCorrelated_Anisotropic( material.alphaT, alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL ); + float D = D_GGX_Anisotropic( material.alphaT, alpha, dotNH, dotTH, dotBH ); + #else + float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); + float D = D_GGX( alpha, dotNH ); + #endif + return F * ( V * D ); +} +vec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) { + const float LUT_SIZE = 64.0; + const float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE; + const float LUT_BIAS = 0.5 / LUT_SIZE; + float dotNV = saturate( dot( N, V ) ); + vec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) ); + uv = uv * LUT_SCALE + LUT_BIAS; + return uv; +} +float LTC_ClippedSphereFormFactor( const in vec3 f ) { + float l = length( f ); + return max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 ); +} +vec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) { + float x = dot( v1, v2 ); + float y = abs( x ); + float a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y; + float b = 3.4175940 + ( 4.1616724 + y ) * y; + float v = a / b; + float theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v; + return cross( v1, v2 ) * theta_sintheta; +} +vec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) { + vec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ]; + vec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ]; + vec3 lightNormal = cross( v1, v2 ); + if( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 ); + vec3 T1, T2; + T1 = normalize( V - N * dot( V, N ) ); + T2 = - cross( N, T1 ); + mat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) ); + vec3 coords[ 4 ]; + coords[ 0 ] = mat * ( rectCoords[ 0 ] - P ); + coords[ 1 ] = mat * ( rectCoords[ 1 ] - P ); + coords[ 2 ] = mat * ( rectCoords[ 2 ] - P ); + coords[ 3 ] = mat * ( rectCoords[ 3 ] - P ); + coords[ 0 ] = normalize( coords[ 0 ] ); + coords[ 1 ] = normalize( coords[ 1 ] ); + coords[ 2 ] = normalize( coords[ 2 ] ); + coords[ 3 ] = normalize( coords[ 3 ] ); + vec3 vectorFormFactor = vec3( 0.0 ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] ); + float result = LTC_ClippedSphereFormFactor( vectorFormFactor ); + return vec3( result ); +} +#if defined( USE_SHEEN ) +float D_Charlie( float roughness, float dotNH ) { + float alpha = pow2( roughness ); + float invAlpha = 1.0 / alpha; + float cos2h = dotNH * dotNH; + float sin2h = max( 1.0 - cos2h, 0.0078125 ); + return ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI ); +} +float V_Neubelt( float dotNV, float dotNL ) { + return saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) ); +} +vec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) { + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float D = D_Charlie( sheenRoughness, dotNH ); + float V = V_Neubelt( dotNV, dotNL ); + return sheenColor * ( D * V ); +} +#endif +float IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { + float dotNV = saturate( dot( normal, viewDir ) ); + float r2 = roughness * roughness; + float a = roughness < 0.25 ? -339.2 * r2 + 161.4 * roughness - 25.9 : -8.48 * r2 + 14.3 * roughness - 9.95; + float b = roughness < 0.25 ? 44.0 * r2 - 23.7 * roughness + 3.26 : 1.97 * r2 - 3.27 * roughness + 0.72; + float DG = exp( a * dotNV + b ) + ( roughness < 0.25 ? 0.0 : 0.1 * ( roughness - 0.25 ) ); + return saturate( DG * RECIPROCAL_PI ); +} +vec2 DFGApprox( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { + float dotNV = saturate( dot( normal, viewDir ) ); + const vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 ); + const vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 ); + vec4 r = roughness * c0 + c1; + float a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y; + vec2 fab = vec2( - 1.04, 1.04 ) * a004 + r.zw; + return fab; +} +vec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) { + vec2 fab = DFGApprox( normal, viewDir, roughness ); + return specularColor * fab.x + specularF90 * fab.y; +} +#ifdef USE_IRIDESCENCE +void computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { +#else +void computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { +#endif + vec2 fab = DFGApprox( normal, viewDir, roughness ); + #ifdef USE_IRIDESCENCE + vec3 Fr = mix( specularColor, iridescenceF0, iridescence ); + #else + vec3 Fr = specularColor; + #endif + vec3 FssEss = Fr * fab.x + specularF90 * fab.y; + float Ess = fab.x + fab.y; + float Ems = 1.0 - Ess; + vec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619; vec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg ); + singleScatter += FssEss; + multiScatter += Fms * Ems; +} +#if NUM_RECT_AREA_LIGHTS > 0 + void RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + vec3 normal = geometryNormal; + vec3 viewDir = geometryViewDir; + vec3 position = geometryPosition; + vec3 lightPos = rectAreaLight.position; + vec3 halfWidth = rectAreaLight.halfWidth; + vec3 halfHeight = rectAreaLight.halfHeight; + vec3 lightColor = rectAreaLight.color; + float roughness = material.roughness; + vec3 rectCoords[ 4 ]; + rectCoords[ 0 ] = lightPos + halfWidth - halfHeight; rectCoords[ 1 ] = lightPos - halfWidth - halfHeight; + rectCoords[ 2 ] = lightPos - halfWidth + halfHeight; + rectCoords[ 3 ] = lightPos + halfWidth + halfHeight; + vec2 uv = LTC_Uv( normal, viewDir, roughness ); + vec4 t1 = texture2D( ltc_1, uv ); + vec4 t2 = texture2D( ltc_2, uv ); + mat3 mInv = mat3( + vec3( t1.x, 0, t1.y ), + vec3( 0, 1, 0 ), + vec3( t1.z, 0, t1.w ) + ); + vec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y ); + reflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords ); + reflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords ); + } +#endif +void RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + #ifdef USE_CLEARCOAT + float dotNLcc = saturate( dot( geometryClearcoatNormal, directLight.direction ) ); + vec3 ccIrradiance = dotNLcc * directLight.color; + clearcoatSpecular += ccIrradiance * BRDF_GGX_Clearcoat( directLight.direction, geometryViewDir, geometryClearcoatNormal, material ); + #endif + #ifdef USE_SHEEN + sheenSpecular += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness ); + #endif + reflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometryViewDir, geometryNormal, material ); + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) { + #ifdef USE_CLEARCOAT + clearcoatSpecular += clearcoatRadiance * EnvironmentBRDF( geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness ); + #endif + #ifdef USE_SHEEN + sheenSpecular += irradiance * material.sheenColor * IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ); + #endif + vec3 singleScattering = vec3( 0.0 ); + vec3 multiScattering = vec3( 0.0 ); + vec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI; + #ifdef USE_IRIDESCENCE + computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness, singleScattering, multiScattering ); + #else + computeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering ); + #endif + vec3 totalScattering = singleScattering + multiScattering; + vec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) ); + reflectedLight.indirectSpecular += radiance * singleScattering; + reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance; + reflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance; +} +#define RE_Direct RE_Direct_Physical +#define RE_Direct_RectArea RE_Direct_RectArea_Physical +#define RE_IndirectDiffuse RE_IndirectDiffuse_Physical +#define RE_IndirectSpecular RE_IndirectSpecular_Physical +float computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) { + return saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion ); +}`,Sm=` +vec3 geometryPosition = - vViewPosition; +vec3 geometryNormal = normal; +vec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition ); +vec3 geometryClearcoatNormal; +#ifdef USE_CLEARCOAT + geometryClearcoatNormal = clearcoatNormal; +#endif +#ifdef USE_IRIDESCENCE + float dotNVi = saturate( dot( normal, geometryViewDir ) ); + if ( material.iridescenceThickness == 0.0 ) { + material.iridescence = 0.0; + } else { + material.iridescence = saturate( material.iridescence ); + } + if ( material.iridescence > 0.0 ) { + material.iridescenceFresnel = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor ); + material.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi ); + } +#endif +IncidentLight directLight; +#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct ) + PointLight pointLight; + #if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0 + PointLightShadow pointLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) { + pointLight = pointLights[ i ]; + getPointLightInfo( pointLight, geometryPosition, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS ) + pointLightShadow = pointLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct ) + SpotLight spotLight; + vec4 spotColor; + vec3 spotLightCoord; + bool inSpotLightMap; + #if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0 + SpotLightShadow spotLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) { + spotLight = spotLights[ i ]; + getSpotLightInfo( spotLight, geometryPosition, directLight ); + #if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) + #define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX + #elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + #define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS + #else + #define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) + #endif + #if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS ) + spotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w; + inSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) ); + spotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy ); + directLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color; + #endif + #undef SPOT_LIGHT_MAP_INDEX + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + spotLightShadow = spotLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) + DirectionalLight directionalLight; + #if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0 + DirectionalLightShadow directionalLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { + directionalLight = directionalLights[ i ]; + getDirectionalLightInfo( directionalLight, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) + directionalLightShadow = directionalLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea ) + RectAreaLight rectAreaLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) { + rectAreaLight = rectAreaLights[ i ]; + RE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if defined( RE_IndirectDiffuse ) + vec3 iblIrradiance = vec3( 0.0 ); + vec3 irradiance = getAmbientLightIrradiance( ambientLightColor ); + #if defined( USE_LIGHT_PROBES ) + irradiance += getLightProbeIrradiance( lightProbe, geometryNormal ); + #endif + #if ( NUM_HEMI_LIGHTS > 0 ) + #pragma unroll_loop_start + for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) { + irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal ); + } + #pragma unroll_loop_end + #endif +#endif +#if defined( RE_IndirectSpecular ) + vec3 radiance = vec3( 0.0 ); + vec3 clearcoatRadiance = vec3( 0.0 ); +#endif`,bm=`#if defined( RE_IndirectDiffuse ) + #ifdef USE_LIGHTMAP + vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); + vec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity; + irradiance += lightMapIrradiance; + #endif + #if defined( USE_ENVMAP ) && defined( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV ) + iblIrradiance += getIBLIrradiance( geometryNormal ); + #endif +#endif +#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular ) + #ifdef USE_ANISOTROPY + radiance += getIBLAnisotropyRadiance( geometryViewDir, geometryNormal, material.roughness, material.anisotropyB, material.anisotropy ); + #else + radiance += getIBLRadiance( geometryViewDir, geometryNormal, material.roughness ); + #endif + #ifdef USE_CLEARCOAT + clearcoatRadiance += getIBLRadiance( geometryViewDir, geometryClearcoatNormal, material.clearcoatRoughness ); + #endif +#endif`,Em=`#if defined( RE_IndirectDiffuse ) + RE_IndirectDiffuse( irradiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); +#endif +#if defined( RE_IndirectSpecular ) + RE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); +#endif`,Tm=`#if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT ) + gl_FragDepthEXT = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5; +#endif`,wm=`#if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT ) + uniform float logDepthBufFC; + varying float vFragDepth; + varying float vIsPerspective; +#endif`,Am=`#ifdef USE_LOGDEPTHBUF + #ifdef USE_LOGDEPTHBUF_EXT + varying float vFragDepth; + varying float vIsPerspective; + #else + uniform float logDepthBufFC; + #endif +#endif`,Rm=`#ifdef USE_LOGDEPTHBUF + #ifdef USE_LOGDEPTHBUF_EXT + vFragDepth = 1.0 + gl_Position.w; + vIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) ); + #else + if ( isPerspectiveMatrix( projectionMatrix ) ) { + gl_Position.z = log2( max( EPSILON, gl_Position.w + 1.0 ) ) * logDepthBufFC - 1.0; + gl_Position.z *= gl_Position.w; + } + #endif +#endif`,Cm=`#ifdef USE_MAP + vec4 sampledDiffuseColor = texture2D( map, vMapUv ); + #ifdef DECODE_VIDEO_TEXTURE + sampledDiffuseColor = vec4( mix( pow( sampledDiffuseColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), sampledDiffuseColor.rgb * 0.0773993808, vec3( lessThanEqual( sampledDiffuseColor.rgb, vec3( 0.04045 ) ) ) ), sampledDiffuseColor.w ); + + #endif + diffuseColor *= sampledDiffuseColor; +#endif`,Pm=`#ifdef USE_MAP + uniform sampler2D map; +#endif`,Lm=`#if defined( USE_MAP ) || defined( USE_ALPHAMAP ) + #if defined( USE_POINTS_UV ) + vec2 uv = vUv; + #else + vec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy; + #endif +#endif +#ifdef USE_MAP + diffuseColor *= texture2D( map, uv ); +#endif +#ifdef USE_ALPHAMAP + diffuseColor.a *= texture2D( alphaMap, uv ).g; +#endif`,Im=`#if defined( USE_POINTS_UV ) + varying vec2 vUv; +#else + #if defined( USE_MAP ) || defined( USE_ALPHAMAP ) + uniform mat3 uvTransform; + #endif +#endif +#ifdef USE_MAP + uniform sampler2D map; +#endif +#ifdef USE_ALPHAMAP + uniform sampler2D alphaMap; +#endif`,Um=`float metalnessFactor = metalness; +#ifdef USE_METALNESSMAP + vec4 texelMetalness = texture2D( metalnessMap, vMetalnessMapUv ); + metalnessFactor *= texelMetalness.b; +#endif`,Dm=`#ifdef USE_METALNESSMAP + uniform sampler2D metalnessMap; +#endif`,Nm=`#if defined( USE_MORPHCOLORS ) && defined( MORPHTARGETS_TEXTURE ) + vColor *= morphTargetBaseInfluence; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + #if defined( USE_COLOR_ALPHA ) + if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ) * morphTargetInfluences[ i ]; + #elif defined( USE_COLOR ) + if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ]; + #endif + } +#endif`,Om=`#ifdef USE_MORPHNORMALS + objectNormal *= morphTargetBaseInfluence; + #ifdef MORPHTARGETS_TEXTURE + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + if ( morphTargetInfluences[ i ] != 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1 ).xyz * morphTargetInfluences[ i ]; + } + #else + objectNormal += morphNormal0 * morphTargetInfluences[ 0 ]; + objectNormal += morphNormal1 * morphTargetInfluences[ 1 ]; + objectNormal += morphNormal2 * morphTargetInfluences[ 2 ]; + objectNormal += morphNormal3 * morphTargetInfluences[ 3 ]; + #endif +#endif`,Fm=`#ifdef USE_MORPHTARGETS + uniform float morphTargetBaseInfluence; + #ifdef MORPHTARGETS_TEXTURE + uniform float morphTargetInfluences[ MORPHTARGETS_COUNT ]; + uniform sampler2DArray morphTargetsTexture; + uniform ivec2 morphTargetsTextureSize; + vec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) { + int texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset; + int y = texelIndex / morphTargetsTextureSize.x; + int x = texelIndex - y * morphTargetsTextureSize.x; + ivec3 morphUV = ivec3( x, y, morphTargetIndex ); + return texelFetch( morphTargetsTexture, morphUV, 0 ); + } + #else + #ifndef USE_MORPHNORMALS + uniform float morphTargetInfluences[ 8 ]; + #else + uniform float morphTargetInfluences[ 4 ]; + #endif + #endif +#endif`,Bm=`#ifdef USE_MORPHTARGETS + transformed *= morphTargetBaseInfluence; + #ifdef MORPHTARGETS_TEXTURE + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + if ( morphTargetInfluences[ i ] != 0.0 ) transformed += getMorph( gl_VertexID, i, 0 ).xyz * morphTargetInfluences[ i ]; + } + #else + transformed += morphTarget0 * morphTargetInfluences[ 0 ]; + transformed += morphTarget1 * morphTargetInfluences[ 1 ]; + transformed += morphTarget2 * morphTargetInfluences[ 2 ]; + transformed += morphTarget3 * morphTargetInfluences[ 3 ]; + #ifndef USE_MORPHNORMALS + transformed += morphTarget4 * morphTargetInfluences[ 4 ]; + transformed += morphTarget5 * morphTargetInfluences[ 5 ]; + transformed += morphTarget6 * morphTargetInfluences[ 6 ]; + transformed += morphTarget7 * morphTargetInfluences[ 7 ]; + #endif + #endif +#endif`,zm=`float faceDirection = gl_FrontFacing ? 1.0 : - 1.0; +#ifdef FLAT_SHADED + vec3 fdx = dFdx( vViewPosition ); + vec3 fdy = dFdy( vViewPosition ); + vec3 normal = normalize( cross( fdx, fdy ) ); +#else + vec3 normal = normalize( vNormal ); + #ifdef DOUBLE_SIDED + normal *= faceDirection; + #endif +#endif +#if defined( USE_NORMALMAP_TANGENTSPACE ) || defined( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) + #ifdef USE_TANGENT + mat3 tbn = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); + #else + mat3 tbn = getTangentFrame( - vViewPosition, normal, + #if defined( USE_NORMALMAP ) + vNormalMapUv + #elif defined( USE_CLEARCOAT_NORMALMAP ) + vClearcoatNormalMapUv + #else + vUv + #endif + ); + #endif + #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) + tbn[0] *= faceDirection; + tbn[1] *= faceDirection; + #endif +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + #ifdef USE_TANGENT + mat3 tbn2 = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); + #else + mat3 tbn2 = getTangentFrame( - vViewPosition, normal, vClearcoatNormalMapUv ); + #endif + #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) + tbn2[0] *= faceDirection; + tbn2[1] *= faceDirection; + #endif +#endif +vec3 nonPerturbedNormal = normal;`,Vm=`#ifdef USE_NORMALMAP_OBJECTSPACE + normal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; + #ifdef FLIP_SIDED + normal = - normal; + #endif + #ifdef DOUBLE_SIDED + normal = normal * faceDirection; + #endif + normal = normalize( normalMatrix * normal ); +#elif defined( USE_NORMALMAP_TANGENTSPACE ) + vec3 mapN = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; + mapN.xy *= normalScale; + normal = normalize( tbn * mapN ); +#elif defined( USE_BUMPMAP ) + normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection ); +#endif`,km=`#ifndef FLAT_SHADED + varying vec3 vNormal; + #ifdef USE_TANGENT + varying vec3 vTangent; + varying vec3 vBitangent; + #endif +#endif`,Hm=`#ifndef FLAT_SHADED + varying vec3 vNormal; + #ifdef USE_TANGENT + varying vec3 vTangent; + varying vec3 vBitangent; + #endif +#endif`,Gm=`#ifndef FLAT_SHADED + vNormal = normalize( transformedNormal ); + #ifdef USE_TANGENT + vTangent = normalize( transformedTangent ); + vBitangent = normalize( cross( vNormal, vTangent ) * tangent.w ); + #endif +#endif`,Wm=`#ifdef USE_NORMALMAP + uniform sampler2D normalMap; + uniform vec2 normalScale; +#endif +#ifdef USE_NORMALMAP_OBJECTSPACE + uniform mat3 normalMatrix; +#endif +#if ! defined ( USE_TANGENT ) && ( defined ( USE_NORMALMAP_TANGENTSPACE ) || defined ( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) ) + mat3 getTangentFrame( vec3 eye_pos, vec3 surf_norm, vec2 uv ) { + vec3 q0 = dFdx( eye_pos.xyz ); + vec3 q1 = dFdy( eye_pos.xyz ); + vec2 st0 = dFdx( uv.st ); + vec2 st1 = dFdy( uv.st ); + vec3 N = surf_norm; + vec3 q1perp = cross( q1, N ); + vec3 q0perp = cross( N, q0 ); + vec3 T = q1perp * st0.x + q0perp * st1.x; + vec3 B = q1perp * st0.y + q0perp * st1.y; + float det = max( dot( T, T ), dot( B, B ) ); + float scale = ( det == 0.0 ) ? 0.0 : inversesqrt( det ); + return mat3( T * scale, B * scale, N ); + } +#endif`,Xm=`#ifdef USE_CLEARCOAT + vec3 clearcoatNormal = nonPerturbedNormal; +#endif`,qm=`#ifdef USE_CLEARCOAT_NORMALMAP + vec3 clearcoatMapN = texture2D( clearcoatNormalMap, vClearcoatNormalMapUv ).xyz * 2.0 - 1.0; + clearcoatMapN.xy *= clearcoatNormalScale; + clearcoatNormal = normalize( tbn2 * clearcoatMapN ); +#endif`,Ym=`#ifdef USE_CLEARCOATMAP + uniform sampler2D clearcoatMap; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + uniform sampler2D clearcoatNormalMap; + uniform vec2 clearcoatNormalScale; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + uniform sampler2D clearcoatRoughnessMap; +#endif`,Zm=`#ifdef USE_IRIDESCENCEMAP + uniform sampler2D iridescenceMap; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + uniform sampler2D iridescenceThicknessMap; +#endif`,Jm=`#ifdef OPAQUE +diffuseColor.a = 1.0; +#endif +#ifdef USE_TRANSMISSION +diffuseColor.a *= material.transmissionAlpha; +#endif +gl_FragColor = vec4( outgoingLight, diffuseColor.a );`,$m=`vec3 packNormalToRGB( const in vec3 normal ) { + return normalize( normal ) * 0.5 + 0.5; +} +vec3 unpackRGBToNormal( const in vec3 rgb ) { + return 2.0 * rgb.xyz - 1.0; +} +const float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.; +const vec3 PackFactors = vec3( 256. * 256. * 256., 256. * 256., 256. ); +const vec4 UnpackFactors = UnpackDownscale / vec4( PackFactors, 1. ); +const float ShiftRight8 = 1. / 256.; +vec4 packDepthToRGBA( const in float v ) { + vec4 r = vec4( fract( v * PackFactors ), v ); + r.yzw -= r.xyz * ShiftRight8; return r * PackUpscale; +} +float unpackRGBAToDepth( const in vec4 v ) { + return dot( v, UnpackFactors ); +} +vec2 packDepthToRG( in highp float v ) { + return packDepthToRGBA( v ).yx; +} +float unpackRGToDepth( const in highp vec2 v ) { + return unpackRGBAToDepth( vec4( v.xy, 0.0, 0.0 ) ); +} +vec4 pack2HalfToRGBA( vec2 v ) { + vec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ) ); + return vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w ); +} +vec2 unpackRGBATo2Half( vec4 v ) { + return vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) ); +} +float viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) { + return ( viewZ + near ) / ( near - far ); +} +float orthographicDepthToViewZ( const in float depth, const in float near, const in float far ) { + return depth * ( near - far ) - near; +} +float viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) { + return ( ( near + viewZ ) * far ) / ( ( far - near ) * viewZ ); +} +float perspectiveDepthToViewZ( const in float depth, const in float near, const in float far ) { + return ( near * far ) / ( ( far - near ) * depth - far ); +}`,Km=`#ifdef PREMULTIPLIED_ALPHA + gl_FragColor.rgb *= gl_FragColor.a; +#endif`,Qm=`vec4 mvPosition = vec4( transformed, 1.0 ); +#ifdef USE_INSTANCING + mvPosition = instanceMatrix * mvPosition; +#endif +mvPosition = modelViewMatrix * mvPosition; +gl_Position = projectionMatrix * mvPosition;`,jm=`#ifdef DITHERING + gl_FragColor.rgb = dithering( gl_FragColor.rgb ); +#endif`,eg=`#ifdef DITHERING + vec3 dithering( vec3 color ) { + float grid_position = rand( gl_FragCoord.xy ); + vec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 ); + dither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position ); + return color + dither_shift_RGB; + } +#endif`,tg=`float roughnessFactor = roughness; +#ifdef USE_ROUGHNESSMAP + vec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv ); + roughnessFactor *= texelRoughness.g; +#endif`,ng=`#ifdef USE_ROUGHNESSMAP + uniform sampler2D roughnessMap; +#endif`,ig=`#if NUM_SPOT_LIGHT_COORDS > 0 + varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; +#endif +#if NUM_SPOT_LIGHT_MAPS > 0 + uniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ]; +#endif +#ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + uniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ]; + varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; + struct DirectionalLightShadow { + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + uniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ]; + struct SpotLightShadow { + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + uniform sampler2D pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ]; + varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; + struct PointLightShadow { + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + float shadowCameraNear; + float shadowCameraFar; + }; + uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; + #endif + float texture2DCompare( sampler2D depths, vec2 uv, float compare ) { + return step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) ); + } + vec2 texture2DDistribution( sampler2D shadow, vec2 uv ) { + return unpackRGBATo2Half( texture2D( shadow, uv ) ); + } + float VSMShadow (sampler2D shadow, vec2 uv, float compare ){ + float occlusion = 1.0; + vec2 distribution = texture2DDistribution( shadow, uv ); + float hard_shadow = step( compare , distribution.x ); + if (hard_shadow != 1.0 ) { + float distance = compare - distribution.x ; + float variance = max( 0.00000, distribution.y * distribution.y ); + float softness_probability = variance / (variance + distance * distance ); softness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 ); occlusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 ); + } + return occlusion; + } + float getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord ) { + float shadow = 1.0; + shadowCoord.xyz /= shadowCoord.w; + shadowCoord.z += shadowBias; + bool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0; + bool frustumTest = inFrustum && shadowCoord.z <= 1.0; + if ( frustumTest ) { + #if defined( SHADOWMAP_TYPE_PCF ) + vec2 texelSize = vec2( 1.0 ) / shadowMapSize; + float dx0 = - texelSize.x * shadowRadius; + float dy0 = - texelSize.y * shadowRadius; + float dx1 = + texelSize.x * shadowRadius; + float dy1 = + texelSize.y * shadowRadius; + float dx2 = dx0 / 2.0; + float dy2 = dy0 / 2.0; + float dx3 = dx1 / 2.0; + float dy3 = dy1 / 2.0; + shadow = ( + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z ) + ) * ( 1.0 / 17.0 ); + #elif defined( SHADOWMAP_TYPE_PCF_SOFT ) + vec2 texelSize = vec2( 1.0 ) / shadowMapSize; + float dx = texelSize.x; + float dy = texelSize.y; + vec2 uv = shadowCoord.xy; + vec2 f = fract( uv * shadowMapSize + 0.5 ); + uv -= f * texelSize; + shadow = ( + texture2DCompare( shadowMap, uv, shadowCoord.z ) + + texture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) + + texture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) + + mix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ), + f.x ) + + mix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ), + f.x ) + + mix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ), + f.y ) + + mix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ), + f.y ) + + mix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ), + f.x ), + mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ), + f.x ), + f.y ) + ) * ( 1.0 / 9.0 ); + #elif defined( SHADOWMAP_TYPE_VSM ) + shadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z ); + #else + shadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ); + #endif + } + return shadow; + } + vec2 cubeToUV( vec3 v, float texelSizeY ) { + vec3 absV = abs( v ); + float scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) ); + absV *= scaleToCube; + v *= scaleToCube * ( 1.0 - 2.0 * texelSizeY ); + vec2 planar = v.xy; + float almostATexel = 1.5 * texelSizeY; + float almostOne = 1.0 - almostATexel; + if ( absV.z >= almostOne ) { + if ( v.z > 0.0 ) + planar.x = 4.0 - v.x; + } else if ( absV.x >= almostOne ) { + float signX = sign( v.x ); + planar.x = v.z * signX + 2.0 * signX; + } else if ( absV.y >= almostOne ) { + float signY = sign( v.y ); + planar.x = v.x + 2.0 * signY + 2.0; + planar.y = v.z * signY - 2.0; + } + return vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 ); + } + float getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) { + vec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) ); + vec3 lightToPosition = shadowCoord.xyz; + float dp = ( length( lightToPosition ) - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear ); dp += shadowBias; + vec3 bd3D = normalize( lightToPosition ); + #if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT ) || defined( SHADOWMAP_TYPE_VSM ) + vec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y; + return ( + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp ) + ) * ( 1.0 / 9.0 ); + #else + return texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ); + #endif + } +#endif`,sg=`#if NUM_SPOT_LIGHT_COORDS > 0 + uniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ]; + varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; +#endif +#ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + uniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ]; + varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; + struct DirectionalLightShadow { + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + struct SpotLightShadow { + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + uniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ]; + varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; + struct PointLightShadow { + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + float shadowCameraNear; + float shadowCameraFar; + }; + uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; + #endif +#endif`,rg=`#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 ) + vec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); + vec4 shadowWorldPosition; +#endif +#if defined( USE_SHADOWMAP ) + #if NUM_DIR_LIGHT_SHADOWS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { + shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 ); + vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { + shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 ); + vPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end + #endif +#endif +#if NUM_SPOT_LIGHT_COORDS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHT_COORDS; i ++ ) { + shadowWorldPosition = worldPosition; + #if ( defined( USE_SHADOWMAP ) && UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + shadowWorldPosition.xyz += shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias; + #endif + vSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end +#endif`,ag=`float getShadowMask() { + float shadow = 1.0; + #ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + DirectionalLightShadow directionalLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { + directionalLight = directionalLightShadows[ i ]; + shadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; + } + #pragma unroll_loop_end + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + SpotLightShadow spotLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) { + spotLight = spotLightShadows[ i ]; + shadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowBias, spotLight.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; + } + #pragma unroll_loop_end + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + PointLightShadow pointLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { + pointLight = pointLightShadows[ i ]; + shadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0; + } + #pragma unroll_loop_end + #endif + #endif + return shadow; +}`,og=`#ifdef USE_SKINNING + mat4 boneMatX = getBoneMatrix( skinIndex.x ); + mat4 boneMatY = getBoneMatrix( skinIndex.y ); + mat4 boneMatZ = getBoneMatrix( skinIndex.z ); + mat4 boneMatW = getBoneMatrix( skinIndex.w ); +#endif`,cg=`#ifdef USE_SKINNING + uniform mat4 bindMatrix; + uniform mat4 bindMatrixInverse; + uniform highp sampler2D boneTexture; + uniform int boneTextureSize; + mat4 getBoneMatrix( const in float i ) { + float j = i * 4.0; + float x = mod( j, float( boneTextureSize ) ); + float y = floor( j / float( boneTextureSize ) ); + float dx = 1.0 / float( boneTextureSize ); + float dy = 1.0 / float( boneTextureSize ); + y = dy * ( y + 0.5 ); + vec4 v1 = texture2D( boneTexture, vec2( dx * ( x + 0.5 ), y ) ); + vec4 v2 = texture2D( boneTexture, vec2( dx * ( x + 1.5 ), y ) ); + vec4 v3 = texture2D( boneTexture, vec2( dx * ( x + 2.5 ), y ) ); + vec4 v4 = texture2D( boneTexture, vec2( dx * ( x + 3.5 ), y ) ); + mat4 bone = mat4( v1, v2, v3, v4 ); + return bone; + } +#endif`,lg=`#ifdef USE_SKINNING + vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 ); + vec4 skinned = vec4( 0.0 ); + skinned += boneMatX * skinVertex * skinWeight.x; + skinned += boneMatY * skinVertex * skinWeight.y; + skinned += boneMatZ * skinVertex * skinWeight.z; + skinned += boneMatW * skinVertex * skinWeight.w; + transformed = ( bindMatrixInverse * skinned ).xyz; +#endif`,hg=`#ifdef USE_SKINNING + mat4 skinMatrix = mat4( 0.0 ); + skinMatrix += skinWeight.x * boneMatX; + skinMatrix += skinWeight.y * boneMatY; + skinMatrix += skinWeight.z * boneMatZ; + skinMatrix += skinWeight.w * boneMatW; + skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix; + objectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz; + #ifdef USE_TANGENT + objectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz; + #endif +#endif`,ug=`float specularStrength; +#ifdef USE_SPECULARMAP + vec4 texelSpecular = texture2D( specularMap, vSpecularMapUv ); + specularStrength = texelSpecular.r; +#else + specularStrength = 1.0; +#endif`,dg=`#ifdef USE_SPECULARMAP + uniform sampler2D specularMap; +#endif`,fg=`#if defined( TONE_MAPPING ) + gl_FragColor.rgb = toneMapping( gl_FragColor.rgb ); +#endif`,pg=`#ifndef saturate +#define saturate( a ) clamp( a, 0.0, 1.0 ) +#endif +uniform float toneMappingExposure; +vec3 LinearToneMapping( vec3 color ) { + return saturate( toneMappingExposure * color ); +} +vec3 ReinhardToneMapping( vec3 color ) { + color *= toneMappingExposure; + return saturate( color / ( vec3( 1.0 ) + color ) ); +} +vec3 OptimizedCineonToneMapping( vec3 color ) { + color *= toneMappingExposure; + color = max( vec3( 0.0 ), color - 0.004 ); + return pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) ); +} +vec3 RRTAndODTFit( vec3 v ) { + vec3 a = v * ( v + 0.0245786 ) - 0.000090537; + vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081; + return a / b; +} +vec3 ACESFilmicToneMapping( vec3 color ) { + const mat3 ACESInputMat = mat3( + vec3( 0.59719, 0.07600, 0.02840 ), vec3( 0.35458, 0.90834, 0.13383 ), + vec3( 0.04823, 0.01566, 0.83777 ) + ); + const mat3 ACESOutputMat = mat3( + vec3( 1.60475, -0.10208, -0.00327 ), vec3( -0.53108, 1.10813, -0.07276 ), + vec3( -0.07367, -0.00605, 1.07602 ) + ); + color *= toneMappingExposure / 0.6; + color = ACESInputMat * color; + color = RRTAndODTFit( color ); + color = ACESOutputMat * color; + return saturate( color ); +} +vec3 CustomToneMapping( vec3 color ) { return color; }`,mg=`#ifdef USE_TRANSMISSION + material.transmission = transmission; + material.transmissionAlpha = 1.0; + material.thickness = thickness; + material.attenuationDistance = attenuationDistance; + material.attenuationColor = attenuationColor; + #ifdef USE_TRANSMISSIONMAP + material.transmission *= texture2D( transmissionMap, vTransmissionMapUv ).r; + #endif + #ifdef USE_THICKNESSMAP + material.thickness *= texture2D( thicknessMap, vThicknessMapUv ).g; + #endif + vec3 pos = vWorldPosition; + vec3 v = normalize( cameraPosition - pos ); + vec3 n = inverseTransformDirection( normal, viewMatrix ); + vec4 transmitted = getIBLVolumeRefraction( + n, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90, + pos, modelMatrix, viewMatrix, projectionMatrix, material.ior, material.thickness, + material.attenuationColor, material.attenuationDistance ); + material.transmissionAlpha = mix( material.transmissionAlpha, transmitted.a, material.transmission ); + totalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission ); +#endif`,gg=`#ifdef USE_TRANSMISSION + uniform float transmission; + uniform float thickness; + uniform float attenuationDistance; + uniform vec3 attenuationColor; + #ifdef USE_TRANSMISSIONMAP + uniform sampler2D transmissionMap; + #endif + #ifdef USE_THICKNESSMAP + uniform sampler2D thicknessMap; + #endif + uniform vec2 transmissionSamplerSize; + uniform sampler2D transmissionSamplerMap; + uniform mat4 modelMatrix; + uniform mat4 projectionMatrix; + varying vec3 vWorldPosition; + float w0( float a ) { + return ( 1.0 / 6.0 ) * ( a * ( a * ( - a + 3.0 ) - 3.0 ) + 1.0 ); + } + float w1( float a ) { + return ( 1.0 / 6.0 ) * ( a * a * ( 3.0 * a - 6.0 ) + 4.0 ); + } + float w2( float a ){ + return ( 1.0 / 6.0 ) * ( a * ( a * ( - 3.0 * a + 3.0 ) + 3.0 ) + 1.0 ); + } + float w3( float a ) { + return ( 1.0 / 6.0 ) * ( a * a * a ); + } + float g0( float a ) { + return w0( a ) + w1( a ); + } + float g1( float a ) { + return w2( a ) + w3( a ); + } + float h0( float a ) { + return - 1.0 + w1( a ) / ( w0( a ) + w1( a ) ); + } + float h1( float a ) { + return 1.0 + w3( a ) / ( w2( a ) + w3( a ) ); + } + vec4 bicubic( sampler2D tex, vec2 uv, vec4 texelSize, float lod ) { + uv = uv * texelSize.zw + 0.5; + vec2 iuv = floor( uv ); + vec2 fuv = fract( uv ); + float g0x = g0( fuv.x ); + float g1x = g1( fuv.x ); + float h0x = h0( fuv.x ); + float h1x = h1( fuv.x ); + float h0y = h0( fuv.y ); + float h1y = h1( fuv.y ); + vec2 p0 = ( vec2( iuv.x + h0x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; + vec2 p1 = ( vec2( iuv.x + h1x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; + vec2 p2 = ( vec2( iuv.x + h0x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; + vec2 p3 = ( vec2( iuv.x + h1x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; + return g0( fuv.y ) * ( g0x * textureLod( tex, p0, lod ) + g1x * textureLod( tex, p1, lod ) ) + + g1( fuv.y ) * ( g0x * textureLod( tex, p2, lod ) + g1x * textureLod( tex, p3, lod ) ); + } + vec4 textureBicubic( sampler2D sampler, vec2 uv, float lod ) { + vec2 fLodSize = vec2( textureSize( sampler, int( lod ) ) ); + vec2 cLodSize = vec2( textureSize( sampler, int( lod + 1.0 ) ) ); + vec2 fLodSizeInv = 1.0 / fLodSize; + vec2 cLodSizeInv = 1.0 / cLodSize; + vec4 fSample = bicubic( sampler, uv, vec4( fLodSizeInv, fLodSize ), floor( lod ) ); + vec4 cSample = bicubic( sampler, uv, vec4( cLodSizeInv, cLodSize ), ceil( lod ) ); + return mix( fSample, cSample, fract( lod ) ); + } + vec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) { + vec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior ); + vec3 modelScale; + modelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) ); + modelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) ); + modelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) ); + return normalize( refractionVector ) * thickness * modelScale; + } + float applyIorToRoughness( const in float roughness, const in float ior ) { + return roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 ); + } + vec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) { + float lod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior ); + return textureBicubic( transmissionSamplerMap, fragCoord.xy, lod ); + } + vec3 volumeAttenuation( const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) { + if ( isinf( attenuationDistance ) ) { + return vec3( 1.0 ); + } else { + vec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance; + vec3 transmittance = exp( - attenuationCoefficient * transmissionDistance ); return transmittance; + } + } + vec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor, + const in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix, + const in mat4 viewMatrix, const in mat4 projMatrix, const in float ior, const in float thickness, + const in vec3 attenuationColor, const in float attenuationDistance ) { + vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix ); + vec3 refractedRayExit = position + transmissionRay; + vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); + vec2 refractionCoords = ndcPos.xy / ndcPos.w; + refractionCoords += 1.0; + refractionCoords /= 2.0; + vec4 transmittedLight = getTransmissionSample( refractionCoords, roughness, ior ); + vec3 transmittance = diffuseColor * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance ); + vec3 attenuatedColor = transmittance * transmittedLight.rgb; + vec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness ); + float transmittanceFactor = ( transmittance.r + transmittance.g + transmittance.b ) / 3.0; + return vec4( ( 1.0 - F ) * attenuatedColor, 1.0 - ( 1.0 - transmittedLight.a ) * transmittanceFactor ); + } +#endif`,_g=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + varying vec2 vUv; +#endif +#ifdef USE_MAP + varying vec2 vMapUv; +#endif +#ifdef USE_ALPHAMAP + varying vec2 vAlphaMapUv; +#endif +#ifdef USE_LIGHTMAP + varying vec2 vLightMapUv; +#endif +#ifdef USE_AOMAP + varying vec2 vAoMapUv; +#endif +#ifdef USE_BUMPMAP + varying vec2 vBumpMapUv; +#endif +#ifdef USE_NORMALMAP + varying vec2 vNormalMapUv; +#endif +#ifdef USE_EMISSIVEMAP + varying vec2 vEmissiveMapUv; +#endif +#ifdef USE_METALNESSMAP + varying vec2 vMetalnessMapUv; +#endif +#ifdef USE_ROUGHNESSMAP + varying vec2 vRoughnessMapUv; +#endif +#ifdef USE_ANISOTROPYMAP + varying vec2 vAnisotropyMapUv; +#endif +#ifdef USE_CLEARCOATMAP + varying vec2 vClearcoatMapUv; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + varying vec2 vClearcoatNormalMapUv; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + varying vec2 vClearcoatRoughnessMapUv; +#endif +#ifdef USE_IRIDESCENCEMAP + varying vec2 vIridescenceMapUv; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + varying vec2 vIridescenceThicknessMapUv; +#endif +#ifdef USE_SHEEN_COLORMAP + varying vec2 vSheenColorMapUv; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + varying vec2 vSheenRoughnessMapUv; +#endif +#ifdef USE_SPECULARMAP + varying vec2 vSpecularMapUv; +#endif +#ifdef USE_SPECULAR_COLORMAP + varying vec2 vSpecularColorMapUv; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + varying vec2 vSpecularIntensityMapUv; +#endif +#ifdef USE_TRANSMISSIONMAP + uniform mat3 transmissionMapTransform; + varying vec2 vTransmissionMapUv; +#endif +#ifdef USE_THICKNESSMAP + uniform mat3 thicknessMapTransform; + varying vec2 vThicknessMapUv; +#endif`,xg=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + varying vec2 vUv; +#endif +#ifdef USE_MAP + uniform mat3 mapTransform; + varying vec2 vMapUv; +#endif +#ifdef USE_ALPHAMAP + uniform mat3 alphaMapTransform; + varying vec2 vAlphaMapUv; +#endif +#ifdef USE_LIGHTMAP + uniform mat3 lightMapTransform; + varying vec2 vLightMapUv; +#endif +#ifdef USE_AOMAP + uniform mat3 aoMapTransform; + varying vec2 vAoMapUv; +#endif +#ifdef USE_BUMPMAP + uniform mat3 bumpMapTransform; + varying vec2 vBumpMapUv; +#endif +#ifdef USE_NORMALMAP + uniform mat3 normalMapTransform; + varying vec2 vNormalMapUv; +#endif +#ifdef USE_DISPLACEMENTMAP + uniform mat3 displacementMapTransform; + varying vec2 vDisplacementMapUv; +#endif +#ifdef USE_EMISSIVEMAP + uniform mat3 emissiveMapTransform; + varying vec2 vEmissiveMapUv; +#endif +#ifdef USE_METALNESSMAP + uniform mat3 metalnessMapTransform; + varying vec2 vMetalnessMapUv; +#endif +#ifdef USE_ROUGHNESSMAP + uniform mat3 roughnessMapTransform; + varying vec2 vRoughnessMapUv; +#endif +#ifdef USE_ANISOTROPYMAP + uniform mat3 anisotropyMapTransform; + varying vec2 vAnisotropyMapUv; +#endif +#ifdef USE_CLEARCOATMAP + uniform mat3 clearcoatMapTransform; + varying vec2 vClearcoatMapUv; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + uniform mat3 clearcoatNormalMapTransform; + varying vec2 vClearcoatNormalMapUv; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + uniform mat3 clearcoatRoughnessMapTransform; + varying vec2 vClearcoatRoughnessMapUv; +#endif +#ifdef USE_SHEEN_COLORMAP + uniform mat3 sheenColorMapTransform; + varying vec2 vSheenColorMapUv; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + uniform mat3 sheenRoughnessMapTransform; + varying vec2 vSheenRoughnessMapUv; +#endif +#ifdef USE_IRIDESCENCEMAP + uniform mat3 iridescenceMapTransform; + varying vec2 vIridescenceMapUv; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + uniform mat3 iridescenceThicknessMapTransform; + varying vec2 vIridescenceThicknessMapUv; +#endif +#ifdef USE_SPECULARMAP + uniform mat3 specularMapTransform; + varying vec2 vSpecularMapUv; +#endif +#ifdef USE_SPECULAR_COLORMAP + uniform mat3 specularColorMapTransform; + varying vec2 vSpecularColorMapUv; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + uniform mat3 specularIntensityMapTransform; + varying vec2 vSpecularIntensityMapUv; +#endif +#ifdef USE_TRANSMISSIONMAP + uniform mat3 transmissionMapTransform; + varying vec2 vTransmissionMapUv; +#endif +#ifdef USE_THICKNESSMAP + uniform mat3 thicknessMapTransform; + varying vec2 vThicknessMapUv; +#endif`,vg=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + vUv = vec3( uv, 1 ).xy; +#endif +#ifdef USE_MAP + vMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ALPHAMAP + vAlphaMapUv = ( alphaMapTransform * vec3( ALPHAMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_LIGHTMAP + vLightMapUv = ( lightMapTransform * vec3( LIGHTMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_AOMAP + vAoMapUv = ( aoMapTransform * vec3( AOMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_BUMPMAP + vBumpMapUv = ( bumpMapTransform * vec3( BUMPMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_NORMALMAP + vNormalMapUv = ( normalMapTransform * vec3( NORMALMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_DISPLACEMENTMAP + vDisplacementMapUv = ( displacementMapTransform * vec3( DISPLACEMENTMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_EMISSIVEMAP + vEmissiveMapUv = ( emissiveMapTransform * vec3( EMISSIVEMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_METALNESSMAP + vMetalnessMapUv = ( metalnessMapTransform * vec3( METALNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ROUGHNESSMAP + vRoughnessMapUv = ( roughnessMapTransform * vec3( ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ANISOTROPYMAP + vAnisotropyMapUv = ( anisotropyMapTransform * vec3( ANISOTROPYMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOATMAP + vClearcoatMapUv = ( clearcoatMapTransform * vec3( CLEARCOATMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + vClearcoatNormalMapUv = ( clearcoatNormalMapTransform * vec3( CLEARCOAT_NORMALMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + vClearcoatRoughnessMapUv = ( clearcoatRoughnessMapTransform * vec3( CLEARCOAT_ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_IRIDESCENCEMAP + vIridescenceMapUv = ( iridescenceMapTransform * vec3( IRIDESCENCEMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + vIridescenceThicknessMapUv = ( iridescenceThicknessMapTransform * vec3( IRIDESCENCE_THICKNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SHEEN_COLORMAP + vSheenColorMapUv = ( sheenColorMapTransform * vec3( SHEEN_COLORMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + vSheenRoughnessMapUv = ( sheenRoughnessMapTransform * vec3( SHEEN_ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULARMAP + vSpecularMapUv = ( specularMapTransform * vec3( SPECULARMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULAR_COLORMAP + vSpecularColorMapUv = ( specularColorMapTransform * vec3( SPECULAR_COLORMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + vSpecularIntensityMapUv = ( specularIntensityMapTransform * vec3( SPECULAR_INTENSITYMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_TRANSMISSIONMAP + vTransmissionMapUv = ( transmissionMapTransform * vec3( TRANSMISSIONMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_THICKNESSMAP + vThicknessMapUv = ( thicknessMapTransform * vec3( THICKNESSMAP_UV, 1 ) ).xy; +#endif`,yg=`#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0 + vec4 worldPosition = vec4( transformed, 1.0 ); + #ifdef USE_INSTANCING + worldPosition = instanceMatrix * worldPosition; + #endif + worldPosition = modelMatrix * worldPosition; +#endif`,Mg=`varying vec2 vUv; +uniform mat3 uvTransform; +void main() { + vUv = ( uvTransform * vec3( uv, 1 ) ).xy; + gl_Position = vec4( position.xy, 1.0, 1.0 ); +}`,Sg=`uniform sampler2D t2D; +uniform float backgroundIntensity; +varying vec2 vUv; +void main() { + vec4 texColor = texture2D( t2D, vUv ); + #ifdef DECODE_VIDEO_TEXTURE + texColor = vec4( mix( pow( texColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), texColor.rgb * 0.0773993808, vec3( lessThanEqual( texColor.rgb, vec3( 0.04045 ) ) ) ), texColor.w ); + #endif + texColor.rgb *= backgroundIntensity; + gl_FragColor = texColor; + #include + #include +}`,bg=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include + gl_Position.z = gl_Position.w; +}`,Eg=`#ifdef ENVMAP_TYPE_CUBE + uniform samplerCube envMap; +#elif defined( ENVMAP_TYPE_CUBE_UV ) + uniform sampler2D envMap; +#endif +uniform float flipEnvMap; +uniform float backgroundBlurriness; +uniform float backgroundIntensity; +varying vec3 vWorldDirection; +#include +void main() { + #ifdef ENVMAP_TYPE_CUBE + vec4 texColor = textureCube( envMap, vec3( flipEnvMap * vWorldDirection.x, vWorldDirection.yz ) ); + #elif defined( ENVMAP_TYPE_CUBE_UV ) + vec4 texColor = textureCubeUV( envMap, vWorldDirection, backgroundBlurriness ); + #else + vec4 texColor = vec4( 0.0, 0.0, 0.0, 1.0 ); + #endif + texColor.rgb *= backgroundIntensity; + gl_FragColor = texColor; + #include + #include +}`,Tg=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include + gl_Position.z = gl_Position.w; +}`,wg=`uniform samplerCube tCube; +uniform float tFlip; +uniform float opacity; +varying vec3 vWorldDirection; +void main() { + vec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) ); + gl_FragColor = texColor; + gl_FragColor.a *= opacity; + #include + #include +}`,Ag=`#include +#include +#include +#include +#include +#include +#include +varying vec2 vHighPrecisionZW; +void main() { + #include + #include + #ifdef USE_DISPLACEMENTMAP + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + vHighPrecisionZW = gl_Position.zw; +}`,Rg=`#if DEPTH_PACKING == 3200 + uniform float opacity; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +varying vec2 vHighPrecisionZW; +void main() { + #include + vec4 diffuseColor = vec4( 1.0 ); + #if DEPTH_PACKING == 3200 + diffuseColor.a = opacity; + #endif + #include + #include + #include + #include + #include + float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5; + #if DEPTH_PACKING == 3200 + gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity ); + #elif DEPTH_PACKING == 3201 + gl_FragColor = packDepthToRGBA( fragCoordZ ); + #endif +}`,Cg=`#define DISTANCE +varying vec3 vWorldPosition; +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #ifdef USE_DISPLACEMENTMAP + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + vWorldPosition = worldPosition.xyz; +}`,Pg=`#define DISTANCE +uniform vec3 referencePosition; +uniform float nearDistance; +uniform float farDistance; +varying vec3 vWorldPosition; +#include +#include +#include +#include +#include +#include +#include +#include +void main () { + #include + vec4 diffuseColor = vec4( 1.0 ); + #include + #include + #include + #include + float dist = length( vWorldPosition - referencePosition ); + dist = ( dist - nearDistance ) / ( farDistance - nearDistance ); + dist = saturate( dist ); + gl_FragColor = packDepthToRGBA( dist ); +}`,Lg=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include +}`,Ig=`uniform sampler2D tEquirect; +varying vec3 vWorldDirection; +#include +void main() { + vec3 direction = normalize( vWorldDirection ); + vec2 sampleUV = equirectUv( direction ); + gl_FragColor = texture2D( tEquirect, sampleUV ); + #include + #include +}`,Ug=`uniform float scale; +attribute float lineDistance; +varying float vLineDistance; +#include +#include +#include +#include +#include +#include +#include +void main() { + vLineDistance = scale * lineDistance; + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,Dg=`uniform vec3 diffuse; +uniform float opacity; +uniform float dashSize; +uniform float totalSize; +varying float vLineDistance; +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + if ( mod( vLineDistance, totalSize ) > dashSize ) { + discard; + } + vec3 outgoingLight = vec3( 0.0 ); + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include + #include +}`,Ng=`#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #if defined ( USE_ENVMAP ) || defined ( USE_SKINNING ) + #include + #include + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,Og=`uniform vec3 diffuse; +uniform float opacity; +#ifndef FLAT_SHADED + varying vec3 vNormal; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + #include + #include + #include + #include + #include + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + #ifdef USE_LIGHTMAP + vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); + reflectedLight.indirectDiffuse += lightMapTexel.rgb * lightMapIntensity * RECIPROCAL_PI; + #else + reflectedLight.indirectDiffuse += vec3( 1.0 ); + #endif + #include + reflectedLight.indirectDiffuse *= diffuseColor.rgb; + vec3 outgoingLight = reflectedLight.indirectDiffuse; + #include + #include + #include + #include + #include + #include + #include +}`,Fg=`#define LAMBERT +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include + #include +}`,Bg=`#define LAMBERT +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + vec4 diffuseColor = vec4( diffuse, opacity ); + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include + #include +}`,zg=`#define MATCAP +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; +}`,Vg=`#define MATCAP +uniform vec3 diffuse; +uniform float opacity; +uniform sampler2D matcap; +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + #include + #include + #include + #include + #include + #include + #include + vec3 viewDir = normalize( vViewPosition ); + vec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) ); + vec3 y = cross( viewDir, x ); + vec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5; + #ifdef USE_MATCAP + vec4 matcapColor = texture2D( matcap, uv ); + #else + vec4 matcapColor = vec4( vec3( mix( 0.2, 0.8, uv.y ) ), 1.0 ); + #endif + vec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb; + #include + #include + #include + #include + #include + #include +}`,kg=`#define NORMAL +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + varying vec3 vViewPosition; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + vViewPosition = - mvPosition.xyz; +#endif +}`,Hg=`#define NORMAL +uniform float opacity; +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + varying vec3 vViewPosition; +#endif +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + gl_FragColor = vec4( packNormalToRGB( normal ), opacity ); + #ifdef OPAQUE + gl_FragColor.a = 1.0; + #endif +}`,Gg=`#define PHONG +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include + #include +}`,Wg=`#define PHONG +uniform vec3 diffuse; +uniform vec3 emissive; +uniform vec3 specular; +uniform float shininess; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + vec4 diffuseColor = vec4( diffuse, opacity ); + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include + #include +}`,Xg=`#define STANDARD +varying vec3 vViewPosition; +#ifdef USE_TRANSMISSION + varying vec3 vWorldPosition; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include +#ifdef USE_TRANSMISSION + vWorldPosition = worldPosition.xyz; +#endif +}`,qg=`#define STANDARD +#ifdef PHYSICAL + #define IOR + #define USE_SPECULAR +#endif +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float roughness; +uniform float metalness; +uniform float opacity; +#ifdef IOR + uniform float ior; +#endif +#ifdef USE_SPECULAR + uniform float specularIntensity; + uniform vec3 specularColor; + #ifdef USE_SPECULAR_COLORMAP + uniform sampler2D specularColorMap; + #endif + #ifdef USE_SPECULAR_INTENSITYMAP + uniform sampler2D specularIntensityMap; + #endif +#endif +#ifdef USE_CLEARCOAT + uniform float clearcoat; + uniform float clearcoatRoughness; +#endif +#ifdef USE_IRIDESCENCE + uniform float iridescence; + uniform float iridescenceIOR; + uniform float iridescenceThicknessMinimum; + uniform float iridescenceThicknessMaximum; +#endif +#ifdef USE_SHEEN + uniform vec3 sheenColor; + uniform float sheenRoughness; + #ifdef USE_SHEEN_COLORMAP + uniform sampler2D sheenColorMap; + #endif + #ifdef USE_SHEEN_ROUGHNESSMAP + uniform sampler2D sheenRoughnessMap; + #endif +#endif +#ifdef USE_ANISOTROPY + uniform vec2 anisotropyVector; + #ifdef USE_ANISOTROPYMAP + uniform sampler2D anisotropyMap; + #endif +#endif +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + vec4 diffuseColor = vec4( diffuse, opacity ); + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse; + vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular; + #include + vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance; + #ifdef USE_SHEEN + float sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor ); + outgoingLight = outgoingLight * sheenEnergyComp + sheenSpecular; + #endif + #ifdef USE_CLEARCOAT + float dotNVcc = saturate( dot( geometryClearcoatNormal, geometryViewDir ) ); + vec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc ); + outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + clearcoatSpecular * material.clearcoat; + #endif + #include + #include + #include + #include + #include + #include +}`,Yg=`#define TOON +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include +}`,Zg=`#define TOON +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + vec4 diffuseColor = vec4( diffuse, opacity ); + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include +}`,Jg=`uniform float size; +uniform float scale; +#include +#include +#include +#include +#include +#include +#ifdef USE_POINTS_UV + varying vec2 vUv; + uniform mat3 uvTransform; +#endif +void main() { + #ifdef USE_POINTS_UV + vUv = ( uvTransform * vec3( uv, 1 ) ).xy; + #endif + #include + #include + #include + #include + #include + gl_PointSize = size; + #ifdef USE_SIZEATTENUATION + bool isPerspective = isPerspectiveMatrix( projectionMatrix ); + if ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z ); + #endif + #include + #include + #include + #include +}`,$g=`uniform vec3 diffuse; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + vec3 outgoingLight = vec3( 0.0 ); + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + #include + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include + #include +}`,Kg=`#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,Qg=`uniform vec3 color; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + gl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) ); + #include + #include + #include +}`,jg=`uniform float rotation; +uniform vec2 center; +#include +#include +#include +#include +#include +void main() { + #include + vec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 ); + vec2 scale; + scale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) ); + scale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) ); + #ifndef USE_SIZEATTENUATION + bool isPerspective = isPerspectiveMatrix( projectionMatrix ); + if ( isPerspective ) scale *= - mvPosition.z; + #endif + vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale; + vec2 rotatedPosition; + rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y; + rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y; + mvPosition.xy += rotatedPosition; + gl_Position = projectionMatrix * mvPosition; + #include + #include + #include +}`,e_=`uniform vec3 diffuse; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + vec3 outgoingLight = vec3( 0.0 ); + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + #include + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include +}`,ke={alphahash_fragment:Ep,alphahash_pars_fragment:Tp,alphamap_fragment:wp,alphamap_pars_fragment:Ap,alphatest_fragment:Rp,alphatest_pars_fragment:Cp,aomap_fragment:Pp,aomap_pars_fragment:Lp,begin_vertex:Ip,beginnormal_vertex:Up,bsdfs:Dp,iridescence_fragment:Np,bumpmap_pars_fragment:Op,clipping_planes_fragment:Fp,clipping_planes_pars_fragment:Bp,clipping_planes_pars_vertex:zp,clipping_planes_vertex:Vp,color_fragment:kp,color_pars_fragment:Hp,color_pars_vertex:Gp,color_vertex:Wp,common:Xp,cube_uv_reflection_fragment:qp,defaultnormal_vertex:Yp,displacementmap_pars_vertex:Zp,displacementmap_vertex:Jp,emissivemap_fragment:$p,emissivemap_pars_fragment:Kp,colorspace_fragment:Qp,colorspace_pars_fragment:jp,envmap_fragment:em,envmap_common_pars_fragment:tm,envmap_pars_fragment:nm,envmap_pars_vertex:im,envmap_physical_pars_fragment:mm,envmap_vertex:sm,fog_vertex:rm,fog_pars_vertex:am,fog_fragment:om,fog_pars_fragment:cm,gradientmap_pars_fragment:lm,lightmap_fragment:hm,lightmap_pars_fragment:um,lights_lambert_fragment:dm,lights_lambert_pars_fragment:fm,lights_pars_begin:pm,lights_toon_fragment:gm,lights_toon_pars_fragment:_m,lights_phong_fragment:xm,lights_phong_pars_fragment:vm,lights_physical_fragment:ym,lights_physical_pars_fragment:Mm,lights_fragment_begin:Sm,lights_fragment_maps:bm,lights_fragment_end:Em,logdepthbuf_fragment:Tm,logdepthbuf_pars_fragment:wm,logdepthbuf_pars_vertex:Am,logdepthbuf_vertex:Rm,map_fragment:Cm,map_pars_fragment:Pm,map_particle_fragment:Lm,map_particle_pars_fragment:Im,metalnessmap_fragment:Um,metalnessmap_pars_fragment:Dm,morphcolor_vertex:Nm,morphnormal_vertex:Om,morphtarget_pars_vertex:Fm,morphtarget_vertex:Bm,normal_fragment_begin:zm,normal_fragment_maps:Vm,normal_pars_fragment:km,normal_pars_vertex:Hm,normal_vertex:Gm,normalmap_pars_fragment:Wm,clearcoat_normal_fragment_begin:Xm,clearcoat_normal_fragment_maps:qm,clearcoat_pars_fragment:Ym,iridescence_pars_fragment:Zm,opaque_fragment:Jm,packing:$m,premultiplied_alpha_fragment:Km,project_vertex:Qm,dithering_fragment:jm,dithering_pars_fragment:eg,roughnessmap_fragment:tg,roughnessmap_pars_fragment:ng,shadowmap_pars_fragment:ig,shadowmap_pars_vertex:sg,shadowmap_vertex:rg,shadowmask_pars_fragment:ag,skinbase_vertex:og,skinning_pars_vertex:cg,skinning_vertex:lg,skinnormal_vertex:hg,specularmap_fragment:ug,specularmap_pars_fragment:dg,tonemapping_fragment:fg,tonemapping_pars_fragment:pg,transmission_fragment:mg,transmission_pars_fragment:gg,uv_pars_fragment:_g,uv_pars_vertex:xg,uv_vertex:vg,worldpos_vertex:yg,background_vert:Mg,background_frag:Sg,backgroundCube_vert:bg,backgroundCube_frag:Eg,cube_vert:Tg,cube_frag:wg,depth_vert:Ag,depth_frag:Rg,distanceRGBA_vert:Cg,distanceRGBA_frag:Pg,equirect_vert:Lg,equirect_frag:Ig,linedashed_vert:Ug,linedashed_frag:Dg,meshbasic_vert:Ng,meshbasic_frag:Og,meshlambert_vert:Fg,meshlambert_frag:Bg,meshmatcap_vert:zg,meshmatcap_frag:Vg,meshnormal_vert:kg,meshnormal_frag:Hg,meshphong_vert:Gg,meshphong_frag:Wg,meshphysical_vert:Xg,meshphysical_frag:qg,meshtoon_vert:Yg,meshtoon_frag:Zg,points_vert:Jg,points_frag:$g,shadow_vert:Kg,shadow_frag:Qg,sprite_vert:jg,sprite_frag:e_},le={common:{diffuse:{value:new pe(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new He},alphaMap:{value:null},alphaMapTransform:{value:new He},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new He}},envmap:{envMap:{value:null},flipEnvMap:{value:-1},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new He}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new He}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new He},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new He},normalScale:{value:new Z(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new He},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new He}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new He}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new He}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new pe(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMap:{value:[]},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotShadowMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMap:{value:[]},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null}},points:{diffuse:{value:new pe(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new He},alphaTest:{value:0},uvTransform:{value:new He}},sprite:{diffuse:{value:new pe(16777215)},opacity:{value:1},center:{value:new Z(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new He},alphaMap:{value:null},alphaMapTransform:{value:new He},alphaTest:{value:0}}},nn={basic:{uniforms:Lt([le.common,le.specularmap,le.envmap,le.aomap,le.lightmap,le.fog]),vertexShader:ke.meshbasic_vert,fragmentShader:ke.meshbasic_frag},lambert:{uniforms:Lt([le.common,le.specularmap,le.envmap,le.aomap,le.lightmap,le.emissivemap,le.bumpmap,le.normalmap,le.displacementmap,le.fog,le.lights,{emissive:{value:new pe(0)}}]),vertexShader:ke.meshlambert_vert,fragmentShader:ke.meshlambert_frag},phong:{uniforms:Lt([le.common,le.specularmap,le.envmap,le.aomap,le.lightmap,le.emissivemap,le.bumpmap,le.normalmap,le.displacementmap,le.fog,le.lights,{emissive:{value:new pe(0)},specular:{value:new pe(1118481)},shininess:{value:30}}]),vertexShader:ke.meshphong_vert,fragmentShader:ke.meshphong_frag},standard:{uniforms:Lt([le.common,le.envmap,le.aomap,le.lightmap,le.emissivemap,le.bumpmap,le.normalmap,le.displacementmap,le.roughnessmap,le.metalnessmap,le.fog,le.lights,{emissive:{value:new pe(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:ke.meshphysical_vert,fragmentShader:ke.meshphysical_frag},toon:{uniforms:Lt([le.common,le.aomap,le.lightmap,le.emissivemap,le.bumpmap,le.normalmap,le.displacementmap,le.gradientmap,le.fog,le.lights,{emissive:{value:new pe(0)}}]),vertexShader:ke.meshtoon_vert,fragmentShader:ke.meshtoon_frag},matcap:{uniforms:Lt([le.common,le.bumpmap,le.normalmap,le.displacementmap,le.fog,{matcap:{value:null}}]),vertexShader:ke.meshmatcap_vert,fragmentShader:ke.meshmatcap_frag},points:{uniforms:Lt([le.points,le.fog]),vertexShader:ke.points_vert,fragmentShader:ke.points_frag},dashed:{uniforms:Lt([le.common,le.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:ke.linedashed_vert,fragmentShader:ke.linedashed_frag},depth:{uniforms:Lt([le.common,le.displacementmap]),vertexShader:ke.depth_vert,fragmentShader:ke.depth_frag},normal:{uniforms:Lt([le.common,le.bumpmap,le.normalmap,le.displacementmap,{opacity:{value:1}}]),vertexShader:ke.meshnormal_vert,fragmentShader:ke.meshnormal_frag},sprite:{uniforms:Lt([le.sprite,le.fog]),vertexShader:ke.sprite_vert,fragmentShader:ke.sprite_frag},background:{uniforms:{uvTransform:{value:new He},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:ke.background_vert,fragmentShader:ke.background_frag},backgroundCube:{uniforms:{envMap:{value:null},flipEnvMap:{value:-1},backgroundBlurriness:{value:0},backgroundIntensity:{value:1}},vertexShader:ke.backgroundCube_vert,fragmentShader:ke.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:ke.cube_vert,fragmentShader:ke.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:ke.equirect_vert,fragmentShader:ke.equirect_frag},distanceRGBA:{uniforms:Lt([le.common,le.displacementmap,{referencePosition:{value:new A},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:ke.distanceRGBA_vert,fragmentShader:ke.distanceRGBA_frag},shadow:{uniforms:Lt([le.lights,le.fog,{color:{value:new pe(0)},opacity:{value:1}}]),vertexShader:ke.shadow_vert,fragmentShader:ke.shadow_frag}};nn.physical={uniforms:Lt([nn.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new He},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new He},clearcoatNormalScale:{value:new Z(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new He},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new He},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new He},sheen:{value:0},sheenColor:{value:new pe(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new He},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new He},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new He},transmissionSamplerSize:{value:new Z},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new He},attenuationDistance:{value:0},attenuationColor:{value:new pe(0)},specularColor:{value:new pe(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new He},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new He},anisotropyVector:{value:new Z},anisotropyMap:{value:null},anisotropyMapTransform:{value:new He}}]),vertexShader:ke.meshphysical_vert,fragmentShader:ke.meshphysical_frag};var cr={r:0,b:0,g:0};function t_(s,e,t,n,i,r,a){let o=new pe(0),c=r===!0?0:1,l,h,u=null,d=0,f=null;function m(g,p){let v=!1,x=p.isScene===!0?p.background:null;x&&x.isTexture&&(x=(p.backgroundBlurriness>0?t:e).get(x)),x===null?_(o,c):x&&x.isColor&&(_(x,1),v=!0);let y=s.xr.getEnvironmentBlendMode();y==="additive"?n.buffers.color.setClear(0,0,0,1,a):y==="alpha-blend"&&n.buffers.color.setClear(0,0,0,0,a),(s.autoClear||v)&&s.clear(s.autoClearColor,s.autoClearDepth,s.autoClearStencil),x&&(x.isCubeTexture||x.mapping===Vs)?(h===void 0&&(h=new Mt(new Ji(1,1,1),new jt({name:"BackgroundCubeMaterial",uniforms:$i(nn.backgroundCube.uniforms),vertexShader:nn.backgroundCube.vertexShader,fragmentShader:nn.backgroundCube.fragmentShader,side:Ft,depthTest:!1,depthWrite:!1,fog:!1})),h.geometry.deleteAttribute("normal"),h.geometry.deleteAttribute("uv"),h.onBeforeRender=function(b,w,R){this.matrixWorld.copyPosition(R.matrixWorld)},Object.defineProperty(h.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),i.update(h)),h.material.uniforms.envMap.value=x,h.material.uniforms.flipEnvMap.value=x.isCubeTexture&&x.isRenderTargetTexture===!1?-1:1,h.material.uniforms.backgroundBlurriness.value=p.backgroundBlurriness,h.material.uniforms.backgroundIntensity.value=p.backgroundIntensity,h.material.toneMapped=Qe.getTransfer(x.colorSpace)!==nt,(u!==x||d!==x.version||f!==s.toneMapping)&&(h.material.needsUpdate=!0,u=x,d=x.version,f=s.toneMapping),h.layers.enableAll(),g.unshift(h,h.geometry,h.material,0,0,null)):x&&x.isTexture&&(l===void 0&&(l=new Mt(new $r(2,2),new jt({name:"BackgroundMaterial",uniforms:$i(nn.background.uniforms),vertexShader:nn.background.vertexShader,fragmentShader:nn.background.fragmentShader,side:Bn,depthTest:!1,depthWrite:!1,fog:!1})),l.geometry.deleteAttribute("normal"),Object.defineProperty(l.material,"map",{get:function(){return this.uniforms.t2D.value}}),i.update(l)),l.material.uniforms.t2D.value=x,l.material.uniforms.backgroundIntensity.value=p.backgroundIntensity,l.material.toneMapped=Qe.getTransfer(x.colorSpace)!==nt,x.matrixAutoUpdate===!0&&x.updateMatrix(),l.material.uniforms.uvTransform.value.copy(x.matrix),(u!==x||d!==x.version||f!==s.toneMapping)&&(l.material.needsUpdate=!0,u=x,d=x.version,f=s.toneMapping),l.layers.enableAll(),g.unshift(l,l.geometry,l.material,0,0,null))}function _(g,p){g.getRGB(cr,bd(s)),n.buffers.color.setClear(cr.r,cr.g,cr.b,p,a)}return{getClearColor:function(){return o},setClearColor:function(g,p=1){o.set(g),c=p,_(o,c)},getClearAlpha:function(){return c},setClearAlpha:function(g){c=g,_(o,c)},render:m}}function n_(s,e,t,n){let i=s.getParameter(s.MAX_VERTEX_ATTRIBS),r=n.isWebGL2?null:e.get("OES_vertex_array_object"),a=n.isWebGL2||r!==null,o={},c=g(null),l=c,h=!1;function u(U,z,q,H,ne){let W=!1;if(a){let K=_(H,q,z);l!==K&&(l=K,f(l.object)),W=p(U,H,q,ne),W&&v(U,H,q,ne)}else{let K=z.wireframe===!0;(l.geometry!==H.id||l.program!==q.id||l.wireframe!==K)&&(l.geometry=H.id,l.program=q.id,l.wireframe=K,W=!0)}ne!==null&&t.update(ne,s.ELEMENT_ARRAY_BUFFER),(W||h)&&(h=!1,I(U,z,q,H),ne!==null&&s.bindBuffer(s.ELEMENT_ARRAY_BUFFER,t.get(ne).buffer))}function d(){return n.isWebGL2?s.createVertexArray():r.createVertexArrayOES()}function f(U){return n.isWebGL2?s.bindVertexArray(U):r.bindVertexArrayOES(U)}function m(U){return n.isWebGL2?s.deleteVertexArray(U):r.deleteVertexArrayOES(U)}function _(U,z,q){let H=q.wireframe===!0,ne=o[U.id];ne===void 0&&(ne={},o[U.id]=ne);let W=ne[z.id];W===void 0&&(W={},ne[z.id]=W);let K=W[H];return K===void 0&&(K=g(d()),W[H]=K),K}function g(U){let z=[],q=[],H=[];for(let ne=0;ne=0){let fe=ne[G],_e=W[G];if(_e===void 0&&(G==="instanceMatrix"&&U.instanceMatrix&&(_e=U.instanceMatrix),G==="instanceColor"&&U.instanceColor&&(_e=U.instanceColor)),fe===void 0||fe.attribute!==_e||_e&&fe.data!==_e.data)return!0;K++}return l.attributesNum!==K||l.index!==H}function v(U,z,q,H){let ne={},W=z.attributes,K=0,D=q.getAttributes();for(let G in D)if(D[G].location>=0){let fe=W[G];fe===void 0&&(G==="instanceMatrix"&&U.instanceMatrix&&(fe=U.instanceMatrix),G==="instanceColor"&&U.instanceColor&&(fe=U.instanceColor));let _e={};_e.attribute=fe,fe&&fe.data&&(_e.data=fe.data),ne[G]=_e,K++}l.attributes=ne,l.attributesNum=K,l.index=H}function x(){let U=l.newAttributes;for(let z=0,q=U.length;z=0){let he=ne[D];if(he===void 0&&(D==="instanceMatrix"&&U.instanceMatrix&&(he=U.instanceMatrix),D==="instanceColor"&&U.instanceColor&&(he=U.instanceColor)),he!==void 0){let fe=he.normalized,_e=he.itemSize,we=t.get(he);if(we===void 0)continue;let Ee=we.buffer,Te=we.type,Ye=we.bytesPerElement,it=n.isWebGL2===!0&&(Te===s.INT||Te===s.UNSIGNED_INT||he.gpuType===dd);if(he.isInterleavedBufferAttribute){let Ce=he.data,L=Ce.stride,oe=he.offset;if(Ce.isInstancedInterleavedBuffer){for(let X=0;X0&&s.getShaderPrecisionFormat(s.FRAGMENT_SHADER,s.HIGH_FLOAT).precision>0)return"highp";R="mediump"}return R==="mediump"&&s.getShaderPrecisionFormat(s.VERTEX_SHADER,s.MEDIUM_FLOAT).precision>0&&s.getShaderPrecisionFormat(s.FRAGMENT_SHADER,s.MEDIUM_FLOAT).precision>0?"mediump":"lowp"}let a=typeof WebGL2RenderingContext<"u"&&s.constructor.name==="WebGL2RenderingContext",o=t.precision!==void 0?t.precision:"highp",c=r(o);c!==o&&(console.warn("THREE.WebGLRenderer:",o,"not supported, using",c,"instead."),o=c);let l=a||e.has("WEBGL_draw_buffers"),h=t.logarithmicDepthBuffer===!0,u=s.getParameter(s.MAX_TEXTURE_IMAGE_UNITS),d=s.getParameter(s.MAX_VERTEX_TEXTURE_IMAGE_UNITS),f=s.getParameter(s.MAX_TEXTURE_SIZE),m=s.getParameter(s.MAX_CUBE_MAP_TEXTURE_SIZE),_=s.getParameter(s.MAX_VERTEX_ATTRIBS),g=s.getParameter(s.MAX_VERTEX_UNIFORM_VECTORS),p=s.getParameter(s.MAX_VARYING_VECTORS),v=s.getParameter(s.MAX_FRAGMENT_UNIFORM_VECTORS),x=d>0,y=a||e.has("OES_texture_float"),b=x&&y,w=a?s.getParameter(s.MAX_SAMPLES):0;return{isWebGL2:a,drawBuffers:l,getMaxAnisotropy:i,getMaxPrecision:r,precision:o,logarithmicDepthBuffer:h,maxTextures:u,maxVertexTextures:d,maxTextureSize:f,maxCubemapSize:m,maxAttributes:_,maxVertexUniforms:g,maxVaryings:p,maxFragmentUniforms:v,vertexTextures:x,floatFragmentTextures:y,floatVertexTextures:b,maxSamples:w}}function r_(s){let e=this,t=null,n=0,i=!1,r=!1,a=new mn,o=new He,c={value:null,needsUpdate:!1};this.uniform=c,this.numPlanes=0,this.numIntersection=0,this.init=function(u,d){let f=u.length!==0||d||n!==0||i;return i=d,n=u.length,f},this.beginShadows=function(){r=!0,h(null)},this.endShadows=function(){r=!1},this.setGlobalState=function(u,d){t=h(u,d,0)},this.setState=function(u,d,f){let m=u.clippingPlanes,_=u.clipIntersection,g=u.clipShadows,p=s.get(u);if(!i||m===null||m.length===0||r&&!g)r?h(null):l();else{let v=r?0:n,x=v*4,y=p.clippingState||null;c.value=y,y=h(m,d,x,f);for(let b=0;b!==x;++b)y[b]=t[b];p.clippingState=y,this.numIntersection=_?this.numPlanes:0,this.numPlanes+=v}};function l(){c.value!==t&&(c.value=t,c.needsUpdate=n>0),e.numPlanes=n,e.numIntersection=0}function h(u,d,f,m){let _=u!==null?u.length:0,g=null;if(_!==0){if(g=c.value,m!==!0||g===null){let p=f+_*4,v=d.matrixWorldInverse;o.getNormalMatrix(v),(g===null||g.length0){let l=new xo(c.height/2);return l.fromEquirectangularTexture(s,a),e.set(a,l),a.addEventListener("dispose",i),t(l.texture,a.mapping)}else return null}}return a}function i(a){let o=a.target;o.removeEventListener("dispose",i);let c=e.get(o);c!==void 0&&(e.delete(o),c.dispose())}function r(){e=new WeakMap}return{get:n,dispose:r}}var Ls=class extends Cs{constructor(e=-1,t=1,n=1,i=-1,r=.1,a=2e3){super(),this.isOrthographicCamera=!0,this.type="OrthographicCamera",this.zoom=1,this.view=null,this.left=e,this.right=t,this.top=n,this.bottom=i,this.near=r,this.far=a,this.updateProjectionMatrix()}copy(e,t){return super.copy(e,t),this.left=e.left,this.right=e.right,this.top=e.top,this.bottom=e.bottom,this.near=e.near,this.far=e.far,this.zoom=e.zoom,this.view=e.view===null?null:Object.assign({},e.view),this}setViewOffset(e,t,n,i,r,a){this.view===null&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=e,this.view.fullHeight=t,this.view.offsetX=n,this.view.offsetY=i,this.view.width=r,this.view.height=a,this.updateProjectionMatrix()}clearViewOffset(){this.view!==null&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){let e=(this.right-this.left)/(2*this.zoom),t=(this.top-this.bottom)/(2*this.zoom),n=(this.right+this.left)/2,i=(this.top+this.bottom)/2,r=n-e,a=n+e,o=i+t,c=i-t;if(this.view!==null&&this.view.enabled){let l=(this.right-this.left)/this.view.fullWidth/this.zoom,h=(this.top-this.bottom)/this.view.fullHeight/this.zoom;r+=l*this.view.offsetX,a=r+l*this.view.width,o-=h*this.view.offsetY,c=o-h*this.view.height}this.projectionMatrix.makeOrthographic(r,a,o,c,this.near,this.far,this.coordinateSystem),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(e){let t=super.toJSON(e);return t.object.zoom=this.zoom,t.object.left=this.left,t.object.right=this.right,t.object.top=this.top,t.object.bottom=this.bottom,t.object.near=this.near,t.object.far=this.far,this.view!==null&&(t.object.view=Object.assign({},this.view)),t}},Hi=4,hh=[.125,.215,.35,.446,.526,.582],ei=20,$a=new Ls,uh=new pe,Ka=null,jn=(1+Math.sqrt(5))/2,Li=1/jn,dh=[new A(1,1,1),new A(-1,1,1),new A(1,1,-1),new A(-1,1,-1),new A(0,jn,Li),new A(0,jn,-Li),new A(Li,0,jn),new A(-Li,0,jn),new A(jn,Li,0),new A(-jn,Li,0)],Kr=class{constructor(e){this._renderer=e,this._pingPongRenderTarget=null,this._lodMax=0,this._cubeSize=0,this._lodPlanes=[],this._sizeLods=[],this._sigmas=[],this._blurMaterial=null,this._cubemapMaterial=null,this._equirectMaterial=null,this._compileMaterial(this._blurMaterial)}fromScene(e,t=0,n=.1,i=100){Ka=this._renderer.getRenderTarget(),this._setSize(256);let r=this._allocateTargets();return r.depthBuffer=!0,this._sceneToCubeUV(e,n,i,r),t>0&&this._blur(r,0,0,t),this._applyPMREM(r),this._cleanup(r),r}fromEquirectangular(e,t=null){return this._fromTexture(e,t)}fromCubemap(e,t=null){return this._fromTexture(e,t)}compileCubemapShader(){this._cubemapMaterial===null&&(this._cubemapMaterial=mh(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){this._equirectMaterial===null&&(this._equirectMaterial=ph(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),this._cubemapMaterial!==null&&this._cubemapMaterial.dispose(),this._equirectMaterial!==null&&this._equirectMaterial.dispose()}_setSize(e){this._lodMax=Math.floor(Math.log2(e)),this._cubeSize=Math.pow(2,this._lodMax)}_dispose(){this._blurMaterial!==null&&this._blurMaterial.dispose(),this._pingPongRenderTarget!==null&&this._pingPongRenderTarget.dispose();for(let e=0;e2?x:0,x,x),h.setRenderTarget(i),_&&h.render(m,o),h.render(e,o)}m.geometry.dispose(),m.material.dispose(),h.toneMapping=d,h.autoClear=u,e.background=g}_textureToCubeUV(e,t){let n=this._renderer,i=e.mapping===zn||e.mapping===ci;i?(this._cubemapMaterial===null&&(this._cubemapMaterial=mh()),this._cubemapMaterial.uniforms.flipEnvMap.value=e.isRenderTargetTexture===!1?-1:1):this._equirectMaterial===null&&(this._equirectMaterial=ph());let r=i?this._cubemapMaterial:this._equirectMaterial,a=new Mt(this._lodPlanes[0],r),o=r.uniforms;o.envMap.value=e;let c=this._cubeSize;lr(t,0,0,3*c,2*c),n.setRenderTarget(t),n.render(a,$a)}_applyPMREM(e){let t=this._renderer,n=t.autoClear;t.autoClear=!1;for(let i=1;iei&&console.warn(`sigmaRadians, ${r}, is too large and will clip, as it requested ${g} samples when the maximum is set to ${ei}`);let p=[],v=0;for(let R=0;Rx-Hi?i-x+Hi:0),w=4*(this._cubeSize-y);lr(t,b,w,3*y,2*y),c.setRenderTarget(t),c.render(u,$a)}};function o_(s){let e=[],t=[],n=[],i=s,r=s-Hi+1+hh.length;for(let a=0;as-Hi?c=hh[a-s+Hi-1]:a===0&&(c=0),n.push(c);let l=1/(o-2),h=-l,u=1+l,d=[h,h,u,h,u,u,h,h,u,u,h,u],f=6,m=6,_=3,g=2,p=1,v=new Float32Array(_*m*f),x=new Float32Array(g*m*f),y=new Float32Array(p*m*f);for(let w=0;w2?0:-1,M=[R,I,0,R+2/3,I,0,R+2/3,I+1,0,R,I,0,R+2/3,I+1,0,R,I+1,0];v.set(M,_*m*w),x.set(d,g*m*w);let T=[w,w,w,w,w,w];y.set(T,p*m*w)}let b=new Ge;b.setAttribute("position",new et(v,_)),b.setAttribute("uv",new et(x,g)),b.setAttribute("faceIndex",new et(y,p)),e.push(b),i>Hi&&i--}return{lodPlanes:e,sizeLods:t,sigmas:n}}function fh(s,e,t){let n=new qt(s,e,t);return n.texture.mapping=Vs,n.texture.name="PMREM.cubeUv",n.scissorTest=!0,n}function lr(s,e,t,n,i){s.viewport.set(e,t,n,i),s.scissor.set(e,t,n,i)}function c_(s,e,t){let n=new Float32Array(ei),i=new A(0,1,0);return new jt({name:"SphericalGaussianBlur",defines:{n:ei,CUBEUV_TEXEL_WIDTH:1/e,CUBEUV_TEXEL_HEIGHT:1/t,CUBEUV_MAX_MIP:`${s}.0`},uniforms:{envMap:{value:null},samples:{value:1},weights:{value:n},latitudinal:{value:!1},dTheta:{value:0},mipInt:{value:0},poleAxis:{value:i}},vertexShader:Zc(),fragmentShader:` + + precision mediump float; + precision mediump int; + + varying vec3 vOutputDirection; + + uniform sampler2D envMap; + uniform int samples; + uniform float weights[ n ]; + uniform bool latitudinal; + uniform float dTheta; + uniform float mipInt; + uniform vec3 poleAxis; + + #define ENVMAP_TYPE_CUBE_UV + #include + + vec3 getSample( float theta, vec3 axis ) { + + float cosTheta = cos( theta ); + // Rodrigues' axis-angle rotation + vec3 sampleDirection = vOutputDirection * cosTheta + + cross( axis, vOutputDirection ) * sin( theta ) + + axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta ); + + return bilinearCubeUV( envMap, sampleDirection, mipInt ); + + } + + void main() { + + vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection ); + + if ( all( equal( axis, vec3( 0.0 ) ) ) ) { + + axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x ); + + } + + axis = normalize( axis ); + + gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 ); + gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis ); + + for ( int i = 1; i < n; i++ ) { + + if ( i >= samples ) { + + break; + + } + + float theta = dTheta * float( i ); + gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis ); + gl_FragColor.rgb += weights[ i ] * getSample( theta, axis ); + + } + + } + `,blending:Dn,depthTest:!1,depthWrite:!1})}function ph(){return new jt({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:Zc(),fragmentShader:` + + precision mediump float; + precision mediump int; + + varying vec3 vOutputDirection; + + uniform sampler2D envMap; + + #include + + void main() { + + vec3 outputDirection = normalize( vOutputDirection ); + vec2 uv = equirectUv( outputDirection ); + + gl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 ); + + } + `,blending:Dn,depthTest:!1,depthWrite:!1})}function mh(){return new jt({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:Zc(),fragmentShader:` + + precision mediump float; + precision mediump int; + + uniform float flipEnvMap; + + varying vec3 vOutputDirection; + + uniform samplerCube envMap; + + void main() { + + gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) ); + + } + `,blending:Dn,depthTest:!1,depthWrite:!1})}function Zc(){return` + + precision mediump float; + precision mediump int; + + attribute float faceIndex; + + varying vec3 vOutputDirection; + + // RH coordinate system; PMREM face-indexing convention + vec3 getDirection( vec2 uv, float face ) { + + uv = 2.0 * uv - 1.0; + + vec3 direction = vec3( uv, 1.0 ); + + if ( face == 0.0 ) { + + direction = direction.zyx; // ( 1, v, u ) pos x + + } else if ( face == 1.0 ) { + + direction = direction.xzy; + direction.xz *= -1.0; // ( -u, 1, -v ) pos y + + } else if ( face == 2.0 ) { + + direction.x *= -1.0; // ( -u, v, 1 ) pos z + + } else if ( face == 3.0 ) { + + direction = direction.zyx; + direction.xz *= -1.0; // ( -1, v, -u ) neg x + + } else if ( face == 4.0 ) { + + direction = direction.xzy; + direction.xy *= -1.0; // ( -u, -1, v ) neg y + + } else if ( face == 5.0 ) { + + direction.z *= -1.0; // ( u, v, -1 ) neg z + + } + + return direction; + + } + + void main() { + + vOutputDirection = getDirection( uv, faceIndex ); + gl_Position = vec4( position, 1.0 ); + + } + `}function l_(s){let e=new WeakMap,t=null;function n(o){if(o&&o.isTexture){let c=o.mapping,l=c===Ir||c===Ur,h=c===zn||c===ci;if(l||h)if(o.isRenderTargetTexture&&o.needsPMREMUpdate===!0){o.needsPMREMUpdate=!1;let u=e.get(o);return t===null&&(t=new Kr(s)),u=l?t.fromEquirectangular(o,u):t.fromCubemap(o,u),e.set(o,u),u.texture}else{if(e.has(o))return e.get(o).texture;{let u=o.image;if(l&&u&&u.height>0||h&&u&&i(u)){t===null&&(t=new Kr(s));let d=l?t.fromEquirectangular(o):t.fromCubemap(o);return e.set(o,d),o.addEventListener("dispose",r),d.texture}else return null}}}return o}function i(o){let c=0,l=6;for(let h=0;he.maxTextureSize&&(T=Math.ceil(M/e.maxTextureSize),M=e.maxTextureSize);let O=new Float32Array(M*T*4*m),Y=new As(O,M,T,m);Y.type=xn,Y.needsUpdate=!0;let $=I*4;for(let z=0;z0)return s;let i=e*t,r=gh[i];if(r===void 0&&(r=new Float32Array(i),gh[i]=r),e!==0){n.toArray(r,0);for(let a=1,o=0;a!==e;++a)o+=t,s[a].toArray(r,o)}return r}function gt(s,e){if(s.length!==e.length)return!1;for(let t=0,n=s.length;t":" "} ${o}: ${t[a]}`)}return n.join(` +`)}function o0(s){let e=Qe.getPrimaries(Qe.workingColorSpace),t=Qe.getPrimaries(s),n;switch(e===t?n="":e===kr&&t===Vr?n="LinearDisplayP3ToLinearSRGB":e===Vr&&t===kr&&(n="LinearSRGBToLinearDisplayP3"),s){case Mn:case va:return[n,"LinearTransferOETF"];case vt:case qc:return[n,"sRGBTransferOETF"];default:return console.warn("THREE.WebGLProgram: Unsupported color space:",s),[n,"LinearTransferOETF"]}}function bh(s,e,t){let n=s.getShaderParameter(e,s.COMPILE_STATUS),i=s.getShaderInfoLog(e).trim();if(n&&i==="")return"";let r=/ERROR: 0:(\d+)/.exec(i);if(r){let a=parseInt(r[1]);return t.toUpperCase()+` + +`+i+` + +`+a0(s.getShaderSource(e),a)}else return i}function c0(s,e){let t=o0(e);return`vec4 ${s}( vec4 value ) { return ${t[0]}( ${t[1]}( value ) ); }`}function l0(s,e){let t;switch(e){case df:t="Linear";break;case ff:t="Reinhard";break;case pf:t="OptimizedCineon";break;case mf:t="ACESFilmic";break;case gf:t="Custom";break;default:console.warn("THREE.WebGLProgram: Unsupported toneMapping:",e),t="Linear"}return"vec3 "+s+"( vec3 color ) { return "+t+"ToneMapping( color ); }"}function h0(s){return[s.extensionDerivatives||s.envMapCubeUVHeight||s.bumpMap||s.normalMapTangentSpace||s.clearcoatNormalMap||s.flatShading||s.shaderID==="physical"?"#extension GL_OES_standard_derivatives : enable":"",(s.extensionFragDepth||s.logarithmicDepthBuffer)&&s.rendererExtensionFragDepth?"#extension GL_EXT_frag_depth : enable":"",s.extensionDrawBuffers&&s.rendererExtensionDrawBuffers?"#extension GL_EXT_draw_buffers : require":"",(s.extensionShaderTextureLOD||s.envMap||s.transmission)&&s.rendererExtensionShaderTextureLod?"#extension GL_EXT_shader_texture_lod : enable":""].filter(vs).join(` +`)}function u0(s){let e=[];for(let t in s){let n=s[t];n!==!1&&e.push("#define "+t+" "+n)}return e.join(` +`)}function d0(s,e){let t={},n=s.getProgramParameter(e,s.ACTIVE_ATTRIBUTES);for(let i=0;i/gm;function So(s){return s.replace(f0,m0)}var p0=new Map([["encodings_fragment","colorspace_fragment"],["encodings_pars_fragment","colorspace_pars_fragment"],["output_fragment","opaque_fragment"]]);function m0(s,e){let t=ke[e];if(t===void 0){let n=p0.get(e);if(n!==void 0)t=ke[n],console.warn('THREE.WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.',e,n);else throw new Error("Can not resolve #include <"+e+">")}return So(t)}var g0=/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;function wh(s){return s.replace(g0,_0)}function _0(s,e,t,n){let i="";for(let r=parseInt(e);r0&&(g+=` +`),p=[f,"#define SHADER_TYPE "+t.shaderType,"#define SHADER_NAME "+t.shaderName,m].filter(vs).join(` +`),p.length>0&&(p+=` +`)):(g=[Ah(t),"#define SHADER_TYPE "+t.shaderType,"#define SHADER_NAME "+t.shaderName,m,t.instancing?"#define USE_INSTANCING":"",t.instancingColor?"#define USE_INSTANCING_COLOR":"",t.useFog&&t.fog?"#define USE_FOG":"",t.useFog&&t.fogExp2?"#define FOG_EXP2":"",t.map?"#define USE_MAP":"",t.envMap?"#define USE_ENVMAP":"",t.envMap?"#define "+h:"",t.lightMap?"#define USE_LIGHTMAP":"",t.aoMap?"#define USE_AOMAP":"",t.bumpMap?"#define USE_BUMPMAP":"",t.normalMap?"#define USE_NORMALMAP":"",t.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",t.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",t.displacementMap?"#define USE_DISPLACEMENTMAP":"",t.emissiveMap?"#define USE_EMISSIVEMAP":"",t.anisotropy?"#define USE_ANISOTROPY":"",t.anisotropyMap?"#define USE_ANISOTROPYMAP":"",t.clearcoatMap?"#define USE_CLEARCOATMAP":"",t.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",t.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",t.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",t.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",t.specularMap?"#define USE_SPECULARMAP":"",t.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",t.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",t.roughnessMap?"#define USE_ROUGHNESSMAP":"",t.metalnessMap?"#define USE_METALNESSMAP":"",t.alphaMap?"#define USE_ALPHAMAP":"",t.alphaHash?"#define USE_ALPHAHASH":"",t.transmission?"#define USE_TRANSMISSION":"",t.transmissionMap?"#define USE_TRANSMISSIONMAP":"",t.thicknessMap?"#define USE_THICKNESSMAP":"",t.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",t.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",t.mapUv?"#define MAP_UV "+t.mapUv:"",t.alphaMapUv?"#define ALPHAMAP_UV "+t.alphaMapUv:"",t.lightMapUv?"#define LIGHTMAP_UV "+t.lightMapUv:"",t.aoMapUv?"#define AOMAP_UV "+t.aoMapUv:"",t.emissiveMapUv?"#define EMISSIVEMAP_UV "+t.emissiveMapUv:"",t.bumpMapUv?"#define BUMPMAP_UV "+t.bumpMapUv:"",t.normalMapUv?"#define NORMALMAP_UV "+t.normalMapUv:"",t.displacementMapUv?"#define DISPLACEMENTMAP_UV "+t.displacementMapUv:"",t.metalnessMapUv?"#define METALNESSMAP_UV "+t.metalnessMapUv:"",t.roughnessMapUv?"#define ROUGHNESSMAP_UV "+t.roughnessMapUv:"",t.anisotropyMapUv?"#define ANISOTROPYMAP_UV "+t.anisotropyMapUv:"",t.clearcoatMapUv?"#define CLEARCOATMAP_UV "+t.clearcoatMapUv:"",t.clearcoatNormalMapUv?"#define CLEARCOAT_NORMALMAP_UV "+t.clearcoatNormalMapUv:"",t.clearcoatRoughnessMapUv?"#define CLEARCOAT_ROUGHNESSMAP_UV "+t.clearcoatRoughnessMapUv:"",t.iridescenceMapUv?"#define IRIDESCENCEMAP_UV "+t.iridescenceMapUv:"",t.iridescenceThicknessMapUv?"#define IRIDESCENCE_THICKNESSMAP_UV "+t.iridescenceThicknessMapUv:"",t.sheenColorMapUv?"#define SHEEN_COLORMAP_UV "+t.sheenColorMapUv:"",t.sheenRoughnessMapUv?"#define SHEEN_ROUGHNESSMAP_UV "+t.sheenRoughnessMapUv:"",t.specularMapUv?"#define SPECULARMAP_UV "+t.specularMapUv:"",t.specularColorMapUv?"#define SPECULAR_COLORMAP_UV "+t.specularColorMapUv:"",t.specularIntensityMapUv?"#define SPECULAR_INTENSITYMAP_UV "+t.specularIntensityMapUv:"",t.transmissionMapUv?"#define TRANSMISSIONMAP_UV "+t.transmissionMapUv:"",t.thicknessMapUv?"#define THICKNESSMAP_UV "+t.thicknessMapUv:"",t.vertexTangents&&t.flatShading===!1?"#define USE_TANGENT":"",t.vertexColors?"#define USE_COLOR":"",t.vertexAlphas?"#define USE_COLOR_ALPHA":"",t.vertexUv1s?"#define USE_UV1":"",t.vertexUv2s?"#define USE_UV2":"",t.vertexUv3s?"#define USE_UV3":"",t.pointsUvs?"#define USE_POINTS_UV":"",t.flatShading?"#define FLAT_SHADED":"",t.skinning?"#define USE_SKINNING":"",t.morphTargets?"#define USE_MORPHTARGETS":"",t.morphNormals&&t.flatShading===!1?"#define USE_MORPHNORMALS":"",t.morphColors&&t.isWebGL2?"#define USE_MORPHCOLORS":"",t.morphTargetsCount>0&&t.isWebGL2?"#define MORPHTARGETS_TEXTURE":"",t.morphTargetsCount>0&&t.isWebGL2?"#define MORPHTARGETS_TEXTURE_STRIDE "+t.morphTextureStride:"",t.morphTargetsCount>0&&t.isWebGL2?"#define MORPHTARGETS_COUNT "+t.morphTargetsCount:"",t.doubleSided?"#define DOUBLE_SIDED":"",t.flipSided?"#define FLIP_SIDED":"",t.shadowMapEnabled?"#define USE_SHADOWMAP":"",t.shadowMapEnabled?"#define "+c:"",t.sizeAttenuation?"#define USE_SIZEATTENUATION":"",t.numLightProbes>0?"#define USE_LIGHT_PROBES":"",t.useLegacyLights?"#define LEGACY_LIGHTS":"",t.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",t.logarithmicDepthBuffer&&t.rendererExtensionFragDepth?"#define USE_LOGDEPTHBUF_EXT":"","uniform mat4 modelMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform mat4 viewMatrix;","uniform mat3 normalMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;","#ifdef USE_INSTANCING"," attribute mat4 instanceMatrix;","#endif","#ifdef USE_INSTANCING_COLOR"," attribute vec3 instanceColor;","#endif","attribute vec3 position;","attribute vec3 normal;","attribute vec2 uv;","#ifdef USE_UV1"," attribute vec2 uv1;","#endif","#ifdef USE_UV2"," attribute vec2 uv2;","#endif","#ifdef USE_UV3"," attribute vec2 uv3;","#endif","#ifdef USE_TANGENT"," attribute vec4 tangent;","#endif","#if defined( USE_COLOR_ALPHA )"," attribute vec4 color;","#elif defined( USE_COLOR )"," attribute vec3 color;","#endif","#if ( defined( USE_MORPHTARGETS ) && ! defined( MORPHTARGETS_TEXTURE ) )"," attribute vec3 morphTarget0;"," attribute vec3 morphTarget1;"," attribute vec3 morphTarget2;"," attribute vec3 morphTarget3;"," #ifdef USE_MORPHNORMALS"," attribute vec3 morphNormal0;"," attribute vec3 morphNormal1;"," attribute vec3 morphNormal2;"," attribute vec3 morphNormal3;"," #else"," attribute vec3 morphTarget4;"," attribute vec3 morphTarget5;"," attribute vec3 morphTarget6;"," attribute vec3 morphTarget7;"," #endif","#endif","#ifdef USE_SKINNING"," attribute vec4 skinIndex;"," attribute vec4 skinWeight;","#endif",` +`].filter(vs).join(` +`),p=[f,Ah(t),"#define SHADER_TYPE "+t.shaderType,"#define SHADER_NAME "+t.shaderName,m,t.useFog&&t.fog?"#define USE_FOG":"",t.useFog&&t.fogExp2?"#define FOG_EXP2":"",t.map?"#define USE_MAP":"",t.matcap?"#define USE_MATCAP":"",t.envMap?"#define USE_ENVMAP":"",t.envMap?"#define "+l:"",t.envMap?"#define "+h:"",t.envMap?"#define "+u:"",d?"#define CUBEUV_TEXEL_WIDTH "+d.texelWidth:"",d?"#define CUBEUV_TEXEL_HEIGHT "+d.texelHeight:"",d?"#define CUBEUV_MAX_MIP "+d.maxMip+".0":"",t.lightMap?"#define USE_LIGHTMAP":"",t.aoMap?"#define USE_AOMAP":"",t.bumpMap?"#define USE_BUMPMAP":"",t.normalMap?"#define USE_NORMALMAP":"",t.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",t.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",t.emissiveMap?"#define USE_EMISSIVEMAP":"",t.anisotropy?"#define USE_ANISOTROPY":"",t.anisotropyMap?"#define USE_ANISOTROPYMAP":"",t.clearcoat?"#define USE_CLEARCOAT":"",t.clearcoatMap?"#define USE_CLEARCOATMAP":"",t.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",t.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",t.iridescence?"#define USE_IRIDESCENCE":"",t.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",t.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",t.specularMap?"#define USE_SPECULARMAP":"",t.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",t.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",t.roughnessMap?"#define USE_ROUGHNESSMAP":"",t.metalnessMap?"#define USE_METALNESSMAP":"",t.alphaMap?"#define USE_ALPHAMAP":"",t.alphaTest?"#define USE_ALPHATEST":"",t.alphaHash?"#define USE_ALPHAHASH":"",t.sheen?"#define USE_SHEEN":"",t.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",t.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",t.transmission?"#define USE_TRANSMISSION":"",t.transmissionMap?"#define USE_TRANSMISSIONMAP":"",t.thicknessMap?"#define USE_THICKNESSMAP":"",t.vertexTangents&&t.flatShading===!1?"#define USE_TANGENT":"",t.vertexColors||t.instancingColor?"#define USE_COLOR":"",t.vertexAlphas?"#define USE_COLOR_ALPHA":"",t.vertexUv1s?"#define USE_UV1":"",t.vertexUv2s?"#define USE_UV2":"",t.vertexUv3s?"#define USE_UV3":"",t.pointsUvs?"#define USE_POINTS_UV":"",t.gradientMap?"#define USE_GRADIENTMAP":"",t.flatShading?"#define FLAT_SHADED":"",t.doubleSided?"#define DOUBLE_SIDED":"",t.flipSided?"#define FLIP_SIDED":"",t.shadowMapEnabled?"#define USE_SHADOWMAP":"",t.shadowMapEnabled?"#define "+c:"",t.premultipliedAlpha?"#define PREMULTIPLIED_ALPHA":"",t.numLightProbes>0?"#define USE_LIGHT_PROBES":"",t.useLegacyLights?"#define LEGACY_LIGHTS":"",t.decodeVideoTexture?"#define DECODE_VIDEO_TEXTURE":"",t.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",t.logarithmicDepthBuffer&&t.rendererExtensionFragDepth?"#define USE_LOGDEPTHBUF_EXT":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;",t.toneMapping!==Nn?"#define TONE_MAPPING":"",t.toneMapping!==Nn?ke.tonemapping_pars_fragment:"",t.toneMapping!==Nn?l0("toneMapping",t.toneMapping):"",t.dithering?"#define DITHERING":"",t.opaque?"#define OPAQUE":"",ke.colorspace_pars_fragment,c0("linearToOutputTexel",t.outputColorSpace),t.useDepthPacking?"#define DEPTH_PACKING "+t.depthPacking:"",` +`].filter(vs).join(` +`)),a=So(a),a=Eh(a,t),a=Th(a,t),o=So(o),o=Eh(o,t),o=Th(o,t),a=wh(a),o=wh(o),t.isWebGL2&&t.isRawShaderMaterial!==!0&&(v=`#version 300 es +`,g=["precision mediump sampler2DArray;","#define attribute in","#define varying out","#define texture2D texture"].join(` +`)+` +`+g,p=["#define varying in",t.glslVersion===Ol?"":"layout(location = 0) out highp vec4 pc_fragColor;",t.glslVersion===Ol?"":"#define gl_FragColor pc_fragColor","#define gl_FragDepthEXT gl_FragDepth","#define texture2D texture","#define textureCube texture","#define texture2DProj textureProj","#define texture2DLodEXT textureLod","#define texture2DProjLodEXT textureProjLod","#define textureCubeLodEXT textureLod","#define texture2DGradEXT textureGrad","#define texture2DProjGradEXT textureProjGrad","#define textureCubeGradEXT textureGrad"].join(` +`)+` +`+p);let x=v+g+a,y=v+p+o,b=Sh(i,i.VERTEX_SHADER,x),w=Sh(i,i.FRAGMENT_SHADER,y);if(i.attachShader(_,b),i.attachShader(_,w),t.index0AttributeName!==void 0?i.bindAttribLocation(_,0,t.index0AttributeName):t.morphTargets===!0&&i.bindAttribLocation(_,0,"position"),i.linkProgram(_),s.debug.checkShaderErrors){let M=i.getProgramInfoLog(_).trim(),T=i.getShaderInfoLog(b).trim(),O=i.getShaderInfoLog(w).trim(),Y=!0,$=!0;if(i.getProgramParameter(_,i.LINK_STATUS)===!1)if(Y=!1,typeof s.debug.onShaderError=="function")s.debug.onShaderError(i,_,b,w);else{let U=bh(i,b,"vertex"),z=bh(i,w,"fragment");console.error("THREE.WebGLProgram: Shader Error "+i.getError()+" - VALIDATE_STATUS "+i.getProgramParameter(_,i.VALIDATE_STATUS)+` + +Program Info Log: `+M+` +`+U+` +`+z)}else M!==""?console.warn("THREE.WebGLProgram: Program Info Log:",M):(T===""||O==="")&&($=!1);$&&(this.diagnostics={runnable:Y,programLog:M,vertexShader:{log:T,prefix:g},fragmentShader:{log:O,prefix:p}})}i.deleteShader(b),i.deleteShader(w);let R;this.getUniforms=function(){return R===void 0&&(R=new qi(i,_)),R};let I;return this.getAttributes=function(){return I===void 0&&(I=d0(i,_)),I},this.destroy=function(){n.releaseStatesOfProgram(this),i.deleteProgram(_),this.program=void 0},this.type=t.shaderType,this.name=t.shaderName,this.id=r0++,this.cacheKey=e,this.usedTimes=1,this.program=_,this.vertexShader=b,this.fragmentShader=w,this}var E0=0,bo=class{constructor(){this.shaderCache=new Map,this.materialCache=new Map}update(e){let t=e.vertexShader,n=e.fragmentShader,i=this._getShaderStage(t),r=this._getShaderStage(n),a=this._getShaderCacheForMaterial(e);return a.has(i)===!1&&(a.add(i),i.usedTimes++),a.has(r)===!1&&(a.add(r),r.usedTimes++),this}remove(e){let t=this.materialCache.get(e);for(let n of t)n.usedTimes--,n.usedTimes===0&&this.shaderCache.delete(n.code);return this.materialCache.delete(e),this}getVertexShaderID(e){return this._getShaderStage(e.vertexShader).id}getFragmentShaderID(e){return this._getShaderStage(e.fragmentShader).id}dispose(){this.shaderCache.clear(),this.materialCache.clear()}_getShaderCacheForMaterial(e){let t=this.materialCache,n=t.get(e);return n===void 0&&(n=new Set,t.set(e,n)),n}_getShaderStage(e){let t=this.shaderCache,n=t.get(e);return n===void 0&&(n=new Eo(e),t.set(e,n)),n}},Eo=class{constructor(e){this.id=E0++,this.code=e,this.usedTimes=0}};function T0(s,e,t,n,i,r,a){let o=new Rs,c=new bo,l=[],h=i.isWebGL2,u=i.logarithmicDepthBuffer,d=i.vertexTextures,f=i.precision,m={MeshDepthMaterial:"depth",MeshDistanceMaterial:"distanceRGBA",MeshNormalMaterial:"normal",MeshBasicMaterial:"basic",MeshLambertMaterial:"lambert",MeshPhongMaterial:"phong",MeshToonMaterial:"toon",MeshStandardMaterial:"physical",MeshPhysicalMaterial:"physical",MeshMatcapMaterial:"matcap",LineBasicMaterial:"basic",LineDashedMaterial:"dashed",PointsMaterial:"points",ShadowMaterial:"shadow",SpriteMaterial:"sprite"};function _(M){return M===0?"uv":`uv${M}`}function g(M,T,O,Y,$){let U=Y.fog,z=$.geometry,q=M.isMeshStandardMaterial?Y.environment:null,H=(M.isMeshStandardMaterial?t:e).get(M.envMap||q),ne=H&&H.mapping===Vs?H.image.height:null,W=m[M.type];M.precision!==null&&(f=i.getMaxPrecision(M.precision),f!==M.precision&&console.warn("THREE.WebGLProgram.getParameters:",M.precision,"not supported, using",f,"instead."));let K=z.morphAttributes.position||z.morphAttributes.normal||z.morphAttributes.color,D=K!==void 0?K.length:0,G=0;z.morphAttributes.position!==void 0&&(G=1),z.morphAttributes.normal!==void 0&&(G=2),z.morphAttributes.color!==void 0&&(G=3);let he,fe,_e,we;if(W){let tt=nn[W];he=tt.vertexShader,fe=tt.fragmentShader}else he=M.vertexShader,fe=M.fragmentShader,c.update(M),_e=c.getVertexShaderID(M),we=c.getFragmentShaderID(M);let Ee=s.getRenderTarget(),Te=$.isInstancedMesh===!0,Ye=!!M.map,it=!!M.matcap,Ce=!!H,L=!!M.aoMap,oe=!!M.lightMap,X=!!M.bumpMap,ie=!!M.normalMap,J=!!M.displacementMap,Se=!!M.emissiveMap,me=!!M.metalnessMap,ye=!!M.roughnessMap,Ne=M.anisotropy>0,qe=M.clearcoat>0,rt=M.iridescence>0,C=M.sheen>0,S=M.transmission>0,B=Ne&&!!M.anisotropyMap,ee=qe&&!!M.clearcoatMap,j=qe&&!!M.clearcoatNormalMap,te=qe&&!!M.clearcoatRoughnessMap,Me=rt&&!!M.iridescenceMap,re=rt&&!!M.iridescenceThicknessMap,de=C&&!!M.sheenColorMap,Le=C&&!!M.sheenRoughnessMap,Ze=!!M.specularMap,se=!!M.specularColorMap,$e=!!M.specularIntensityMap,Oe=S&&!!M.transmissionMap,Ie=S&&!!M.thicknessMap,Re=!!M.gradientMap,P=!!M.alphaMap,ce=M.alphaTest>0,ae=!!M.alphaHash,ge=!!M.extensions,ue=!!z.attributes.uv1,Q=!!z.attributes.uv2,be=!!z.attributes.uv3,Fe=Nn;return M.toneMapped&&(Ee===null||Ee.isXRRenderTarget===!0)&&(Fe=s.toneMapping),{isWebGL2:h,shaderID:W,shaderType:M.type,shaderName:M.name,vertexShader:he,fragmentShader:fe,defines:M.defines,customVertexShaderID:_e,customFragmentShaderID:we,isRawShaderMaterial:M.isRawShaderMaterial===!0,glslVersion:M.glslVersion,precision:f,instancing:Te,instancingColor:Te&&$.instanceColor!==null,supportsVertexTextures:d,outputColorSpace:Ee===null?s.outputColorSpace:Ee.isXRRenderTarget===!0?Ee.texture.colorSpace:Mn,map:Ye,matcap:it,envMap:Ce,envMapMode:Ce&&H.mapping,envMapCubeUVHeight:ne,aoMap:L,lightMap:oe,bumpMap:X,normalMap:ie,displacementMap:d&&J,emissiveMap:Se,normalMapObjectSpace:ie&&M.normalMapType===Lf,normalMapTangentSpace:ie&&M.normalMapType===mi,metalnessMap:me,roughnessMap:ye,anisotropy:Ne,anisotropyMap:B,clearcoat:qe,clearcoatMap:ee,clearcoatNormalMap:j,clearcoatRoughnessMap:te,iridescence:rt,iridescenceMap:Me,iridescenceThicknessMap:re,sheen:C,sheenColorMap:de,sheenRoughnessMap:Le,specularMap:Ze,specularColorMap:se,specularIntensityMap:$e,transmission:S,transmissionMap:Oe,thicknessMap:Ie,gradientMap:Re,opaque:M.transparent===!1&&M.blending===Wi,alphaMap:P,alphaTest:ce,alphaHash:ae,combine:M.combine,mapUv:Ye&&_(M.map.channel),aoMapUv:L&&_(M.aoMap.channel),lightMapUv:oe&&_(M.lightMap.channel),bumpMapUv:X&&_(M.bumpMap.channel),normalMapUv:ie&&_(M.normalMap.channel),displacementMapUv:J&&_(M.displacementMap.channel),emissiveMapUv:Se&&_(M.emissiveMap.channel),metalnessMapUv:me&&_(M.metalnessMap.channel),roughnessMapUv:ye&&_(M.roughnessMap.channel),anisotropyMapUv:B&&_(M.anisotropyMap.channel),clearcoatMapUv:ee&&_(M.clearcoatMap.channel),clearcoatNormalMapUv:j&&_(M.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:te&&_(M.clearcoatRoughnessMap.channel),iridescenceMapUv:Me&&_(M.iridescenceMap.channel),iridescenceThicknessMapUv:re&&_(M.iridescenceThicknessMap.channel),sheenColorMapUv:de&&_(M.sheenColorMap.channel),sheenRoughnessMapUv:Le&&_(M.sheenRoughnessMap.channel),specularMapUv:Ze&&_(M.specularMap.channel),specularColorMapUv:se&&_(M.specularColorMap.channel),specularIntensityMapUv:$e&&_(M.specularIntensityMap.channel),transmissionMapUv:Oe&&_(M.transmissionMap.channel),thicknessMapUv:Ie&&_(M.thicknessMap.channel),alphaMapUv:P&&_(M.alphaMap.channel),vertexTangents:!!z.attributes.tangent&&(ie||Ne),vertexColors:M.vertexColors,vertexAlphas:M.vertexColors===!0&&!!z.attributes.color&&z.attributes.color.itemSize===4,vertexUv1s:ue,vertexUv2s:Q,vertexUv3s:be,pointsUvs:$.isPoints===!0&&!!z.attributes.uv&&(Ye||P),fog:!!U,useFog:M.fog===!0,fogExp2:U&&U.isFogExp2,flatShading:M.flatShading===!0,sizeAttenuation:M.sizeAttenuation===!0,logarithmicDepthBuffer:u,skinning:$.isSkinnedMesh===!0,morphTargets:z.morphAttributes.position!==void 0,morphNormals:z.morphAttributes.normal!==void 0,morphColors:z.morphAttributes.color!==void 0,morphTargetsCount:D,morphTextureStride:G,numDirLights:T.directional.length,numPointLights:T.point.length,numSpotLights:T.spot.length,numSpotLightMaps:T.spotLightMap.length,numRectAreaLights:T.rectArea.length,numHemiLights:T.hemi.length,numDirLightShadows:T.directionalShadowMap.length,numPointLightShadows:T.pointShadowMap.length,numSpotLightShadows:T.spotShadowMap.length,numSpotLightShadowsWithMaps:T.numSpotLightShadowsWithMaps,numLightProbes:T.numLightProbes,numClippingPlanes:a.numPlanes,numClipIntersection:a.numIntersection,dithering:M.dithering,shadowMapEnabled:s.shadowMap.enabled&&O.length>0,shadowMapType:s.shadowMap.type,toneMapping:Fe,useLegacyLights:s._useLegacyLights,decodeVideoTexture:Ye&&M.map.isVideoTexture===!0&&Qe.getTransfer(M.map.colorSpace)===nt,premultipliedAlpha:M.premultipliedAlpha,doubleSided:M.side===gn,flipSided:M.side===Ft,useDepthPacking:M.depthPacking>=0,depthPacking:M.depthPacking||0,index0AttributeName:M.index0AttributeName,extensionDerivatives:ge&&M.extensions.derivatives===!0,extensionFragDepth:ge&&M.extensions.fragDepth===!0,extensionDrawBuffers:ge&&M.extensions.drawBuffers===!0,extensionShaderTextureLOD:ge&&M.extensions.shaderTextureLOD===!0,rendererExtensionFragDepth:h||n.has("EXT_frag_depth"),rendererExtensionDrawBuffers:h||n.has("WEBGL_draw_buffers"),rendererExtensionShaderTextureLod:h||n.has("EXT_shader_texture_lod"),customProgramCacheKey:M.customProgramCacheKey()}}function p(M){let T=[];if(M.shaderID?T.push(M.shaderID):(T.push(M.customVertexShaderID),T.push(M.customFragmentShaderID)),M.defines!==void 0)for(let O in M.defines)T.push(O),T.push(M.defines[O]);return M.isRawShaderMaterial===!1&&(v(T,M),x(T,M),T.push(s.outputColorSpace)),T.push(M.customProgramCacheKey),T.join()}function v(M,T){M.push(T.precision),M.push(T.outputColorSpace),M.push(T.envMapMode),M.push(T.envMapCubeUVHeight),M.push(T.mapUv),M.push(T.alphaMapUv),M.push(T.lightMapUv),M.push(T.aoMapUv),M.push(T.bumpMapUv),M.push(T.normalMapUv),M.push(T.displacementMapUv),M.push(T.emissiveMapUv),M.push(T.metalnessMapUv),M.push(T.roughnessMapUv),M.push(T.anisotropyMapUv),M.push(T.clearcoatMapUv),M.push(T.clearcoatNormalMapUv),M.push(T.clearcoatRoughnessMapUv),M.push(T.iridescenceMapUv),M.push(T.iridescenceThicknessMapUv),M.push(T.sheenColorMapUv),M.push(T.sheenRoughnessMapUv),M.push(T.specularMapUv),M.push(T.specularColorMapUv),M.push(T.specularIntensityMapUv),M.push(T.transmissionMapUv),M.push(T.thicknessMapUv),M.push(T.combine),M.push(T.fogExp2),M.push(T.sizeAttenuation),M.push(T.morphTargetsCount),M.push(T.morphAttributeCount),M.push(T.numDirLights),M.push(T.numPointLights),M.push(T.numSpotLights),M.push(T.numSpotLightMaps),M.push(T.numHemiLights),M.push(T.numRectAreaLights),M.push(T.numDirLightShadows),M.push(T.numPointLightShadows),M.push(T.numSpotLightShadows),M.push(T.numSpotLightShadowsWithMaps),M.push(T.numLightProbes),M.push(T.shadowMapType),M.push(T.toneMapping),M.push(T.numClippingPlanes),M.push(T.numClipIntersection),M.push(T.depthPacking)}function x(M,T){o.disableAll(),T.isWebGL2&&o.enable(0),T.supportsVertexTextures&&o.enable(1),T.instancing&&o.enable(2),T.instancingColor&&o.enable(3),T.matcap&&o.enable(4),T.envMap&&o.enable(5),T.normalMapObjectSpace&&o.enable(6),T.normalMapTangentSpace&&o.enable(7),T.clearcoat&&o.enable(8),T.iridescence&&o.enable(9),T.alphaTest&&o.enable(10),T.vertexColors&&o.enable(11),T.vertexAlphas&&o.enable(12),T.vertexUv1s&&o.enable(13),T.vertexUv2s&&o.enable(14),T.vertexUv3s&&o.enable(15),T.vertexTangents&&o.enable(16),T.anisotropy&&o.enable(17),M.push(o.mask),o.disableAll(),T.fog&&o.enable(0),T.useFog&&o.enable(1),T.flatShading&&o.enable(2),T.logarithmicDepthBuffer&&o.enable(3),T.skinning&&o.enable(4),T.morphTargets&&o.enable(5),T.morphNormals&&o.enable(6),T.morphColors&&o.enable(7),T.premultipliedAlpha&&o.enable(8),T.shadowMapEnabled&&o.enable(9),T.useLegacyLights&&o.enable(10),T.doubleSided&&o.enable(11),T.flipSided&&o.enable(12),T.useDepthPacking&&o.enable(13),T.dithering&&o.enable(14),T.transmission&&o.enable(15),T.sheen&&o.enable(16),T.opaque&&o.enable(17),T.pointsUvs&&o.enable(18),T.decodeVideoTexture&&o.enable(19),M.push(o.mask)}function y(M){let T=m[M.type],O;if(T){let Y=nn[T];O=xp.clone(Y.uniforms)}else O=M.uniforms;return O}function b(M,T){let O;for(let Y=0,$=l.length;Y<$;Y++){let U=l[Y];if(U.cacheKey===T){O=U,++O.usedTimes;break}}return O===void 0&&(O=new b0(s,T,M,r),l.push(O)),O}function w(M){if(--M.usedTimes===0){let T=l.indexOf(M);l[T]=l[l.length-1],l.pop(),M.destroy()}}function R(M){c.remove(M)}function I(){c.dispose()}return{getParameters:g,getProgramCacheKey:p,getUniforms:y,acquireProgram:b,releaseProgram:w,releaseShaderCache:R,programs:l,dispose:I}}function w0(){let s=new WeakMap;function e(r){let a=s.get(r);return a===void 0&&(a={},s.set(r,a)),a}function t(r){s.delete(r)}function n(r,a,o){s.get(r)[a]=o}function i(){s=new WeakMap}return{get:e,remove:t,update:n,dispose:i}}function A0(s,e){return s.groupOrder!==e.groupOrder?s.groupOrder-e.groupOrder:s.renderOrder!==e.renderOrder?s.renderOrder-e.renderOrder:s.material.id!==e.material.id?s.material.id-e.material.id:s.z!==e.z?s.z-e.z:s.id-e.id}function Rh(s,e){return s.groupOrder!==e.groupOrder?s.groupOrder-e.groupOrder:s.renderOrder!==e.renderOrder?s.renderOrder-e.renderOrder:s.z!==e.z?e.z-s.z:s.id-e.id}function Ch(){let s=[],e=0,t=[],n=[],i=[];function r(){e=0,t.length=0,n.length=0,i.length=0}function a(u,d,f,m,_,g){let p=s[e];return p===void 0?(p={id:u.id,object:u,geometry:d,material:f,groupOrder:m,renderOrder:u.renderOrder,z:_,group:g},s[e]=p):(p.id=u.id,p.object=u,p.geometry=d,p.material=f,p.groupOrder=m,p.renderOrder=u.renderOrder,p.z=_,p.group=g),e++,p}function o(u,d,f,m,_,g){let p=a(u,d,f,m,_,g);f.transmission>0?n.push(p):f.transparent===!0?i.push(p):t.push(p)}function c(u,d,f,m,_,g){let p=a(u,d,f,m,_,g);f.transmission>0?n.unshift(p):f.transparent===!0?i.unshift(p):t.unshift(p)}function l(u,d){t.length>1&&t.sort(u||A0),n.length>1&&n.sort(d||Rh),i.length>1&&i.sort(d||Rh)}function h(){for(let u=e,d=s.length;u=r.length?(a=new Ch,r.push(a)):a=r[i],a}function t(){s=new WeakMap}return{get:e,dispose:t}}function C0(){let s={};return{get:function(e){if(s[e.id]!==void 0)return s[e.id];let t;switch(e.type){case"DirectionalLight":t={direction:new A,color:new pe};break;case"SpotLight":t={position:new A,direction:new A,color:new pe,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":t={position:new A,color:new pe,distance:0,decay:0};break;case"HemisphereLight":t={direction:new A,skyColor:new pe,groundColor:new pe};break;case"RectAreaLight":t={color:new pe,position:new A,halfWidth:new A,halfHeight:new A};break}return s[e.id]=t,t}}}function P0(){let s={};return{get:function(e){if(s[e.id]!==void 0)return s[e.id];let t;switch(e.type){case"DirectionalLight":t={shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Z};break;case"SpotLight":t={shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Z};break;case"PointLight":t={shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Z,shadowCameraNear:1,shadowCameraFar:1e3};break}return s[e.id]=t,t}}}var L0=0;function I0(s,e){return(e.castShadow?2:0)-(s.castShadow?2:0)+(e.map?1:0)-(s.map?1:0)}function U0(s,e){let t=new C0,n=P0(),i={version:0,hash:{directionalLength:-1,pointLength:-1,spotLength:-1,rectAreaLength:-1,hemiLength:-1,numDirectionalShadows:-1,numPointShadows:-1,numSpotShadows:-1,numSpotMaps:-1,numLightProbes:-1},ambient:[0,0,0],probe:[],directional:[],directionalShadow:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotLightMap:[],spotShadow:[],spotShadowMap:[],spotLightMatrix:[],rectArea:[],rectAreaLTC1:null,rectAreaLTC2:null,point:[],pointShadow:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[],numSpotLightShadowsWithMaps:0,numLightProbes:0};for(let h=0;h<9;h++)i.probe.push(new A);let r=new A,a=new ze,o=new ze;function c(h,u){let d=0,f=0,m=0;for(let Y=0;Y<9;Y++)i.probe[Y].set(0,0,0);let _=0,g=0,p=0,v=0,x=0,y=0,b=0,w=0,R=0,I=0,M=0;h.sort(I0);let T=u===!0?Math.PI:1;for(let Y=0,$=h.length;Y<$;Y++){let U=h[Y],z=U.color,q=U.intensity,H=U.distance,ne=U.shadow&&U.shadow.map?U.shadow.map.texture:null;if(U.isAmbientLight)d+=z.r*q*T,f+=z.g*q*T,m+=z.b*q*T;else if(U.isLightProbe){for(let W=0;W<9;W++)i.probe[W].addScaledVector(U.sh.coefficients[W],q);M++}else if(U.isDirectionalLight){let W=t.get(U);if(W.color.copy(U.color).multiplyScalar(U.intensity*T),U.castShadow){let K=U.shadow,D=n.get(U);D.shadowBias=K.bias,D.shadowNormalBias=K.normalBias,D.shadowRadius=K.radius,D.shadowMapSize=K.mapSize,i.directionalShadow[_]=D,i.directionalShadowMap[_]=ne,i.directionalShadowMatrix[_]=U.shadow.matrix,y++}i.directional[_]=W,_++}else if(U.isSpotLight){let W=t.get(U);W.position.setFromMatrixPosition(U.matrixWorld),W.color.copy(z).multiplyScalar(q*T),W.distance=H,W.coneCos=Math.cos(U.angle),W.penumbraCos=Math.cos(U.angle*(1-U.penumbra)),W.decay=U.decay,i.spot[p]=W;let K=U.shadow;if(U.map&&(i.spotLightMap[R]=U.map,R++,K.updateMatrices(U),U.castShadow&&I++),i.spotLightMatrix[p]=K.matrix,U.castShadow){let D=n.get(U);D.shadowBias=K.bias,D.shadowNormalBias=K.normalBias,D.shadowRadius=K.radius,D.shadowMapSize=K.mapSize,i.spotShadow[p]=D,i.spotShadowMap[p]=ne,w++}p++}else if(U.isRectAreaLight){let W=t.get(U);W.color.copy(z).multiplyScalar(q),W.halfWidth.set(U.width*.5,0,0),W.halfHeight.set(0,U.height*.5,0),i.rectArea[v]=W,v++}else if(U.isPointLight){let W=t.get(U);if(W.color.copy(U.color).multiplyScalar(U.intensity*T),W.distance=U.distance,W.decay=U.decay,U.castShadow){let K=U.shadow,D=n.get(U);D.shadowBias=K.bias,D.shadowNormalBias=K.normalBias,D.shadowRadius=K.radius,D.shadowMapSize=K.mapSize,D.shadowCameraNear=K.camera.near,D.shadowCameraFar=K.camera.far,i.pointShadow[g]=D,i.pointShadowMap[g]=ne,i.pointShadowMatrix[g]=U.shadow.matrix,b++}i.point[g]=W,g++}else if(U.isHemisphereLight){let W=t.get(U);W.skyColor.copy(U.color).multiplyScalar(q*T),W.groundColor.copy(U.groundColor).multiplyScalar(q*T),i.hemi[x]=W,x++}}v>0&&(e.isWebGL2||s.has("OES_texture_float_linear")===!0?(i.rectAreaLTC1=le.LTC_FLOAT_1,i.rectAreaLTC2=le.LTC_FLOAT_2):s.has("OES_texture_half_float_linear")===!0?(i.rectAreaLTC1=le.LTC_HALF_1,i.rectAreaLTC2=le.LTC_HALF_2):console.error("THREE.WebGLRenderer: Unable to use RectAreaLight. Missing WebGL extensions.")),i.ambient[0]=d,i.ambient[1]=f,i.ambient[2]=m;let O=i.hash;(O.directionalLength!==_||O.pointLength!==g||O.spotLength!==p||O.rectAreaLength!==v||O.hemiLength!==x||O.numDirectionalShadows!==y||O.numPointShadows!==b||O.numSpotShadows!==w||O.numSpotMaps!==R||O.numLightProbes!==M)&&(i.directional.length=_,i.spot.length=p,i.rectArea.length=v,i.point.length=g,i.hemi.length=x,i.directionalShadow.length=y,i.directionalShadowMap.length=y,i.pointShadow.length=b,i.pointShadowMap.length=b,i.spotShadow.length=w,i.spotShadowMap.length=w,i.directionalShadowMatrix.length=y,i.pointShadowMatrix.length=b,i.spotLightMatrix.length=w+R-I,i.spotLightMap.length=R,i.numSpotLightShadowsWithMaps=I,i.numLightProbes=M,O.directionalLength=_,O.pointLength=g,O.spotLength=p,O.rectAreaLength=v,O.hemiLength=x,O.numDirectionalShadows=y,O.numPointShadows=b,O.numSpotShadows=w,O.numSpotMaps=R,O.numLightProbes=M,i.version=L0++)}function l(h,u){let d=0,f=0,m=0,_=0,g=0,p=u.matrixWorldInverse;for(let v=0,x=h.length;v=o.length?(c=new Ph(s,e),o.push(c)):c=o[a],c}function i(){t=new WeakMap}return{get:n,dispose:i}}var Qr=class extends bt{constructor(e){super(),this.isMeshDepthMaterial=!0,this.type="MeshDepthMaterial",this.depthPacking=Cf,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.setValues(e)}copy(e){return super.copy(e),this.depthPacking=e.depthPacking,this.map=e.map,this.alphaMap=e.alphaMap,this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this}},jr=class extends bt{constructor(e){super(),this.isMeshDistanceMaterial=!0,this.type="MeshDistanceMaterial",this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.setValues(e)}copy(e){return super.copy(e),this.map=e.map,this.alphaMap=e.alphaMap,this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this}},N0=`void main() { + gl_Position = vec4( position, 1.0 ); +}`,O0=`uniform sampler2D shadow_pass; +uniform vec2 resolution; +uniform float radius; +#include +void main() { + const float samples = float( VSM_SAMPLES ); + float mean = 0.0; + float squared_mean = 0.0; + float uvStride = samples <= 1.0 ? 0.0 : 2.0 / ( samples - 1.0 ); + float uvStart = samples <= 1.0 ? 0.0 : - 1.0; + for ( float i = 0.0; i < samples; i ++ ) { + float uvOffset = uvStart + i * uvStride; + #ifdef HORIZONTAL_PASS + vec2 distribution = unpackRGBATo2Half( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ) ); + mean += distribution.x; + squared_mean += distribution.y * distribution.y + distribution.x * distribution.x; + #else + float depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ) ); + mean += depth; + squared_mean += depth * depth; + #endif + } + mean = mean / samples; + squared_mean = squared_mean / samples; + float std_dev = sqrt( squared_mean - mean * mean ); + gl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) ); +}`;function F0(s,e,t){let n=new Ps,i=new Z,r=new Z,a=new je,o=new Qr({depthPacking:Pf}),c=new jr,l={},h=t.maxTextureSize,u={[Bn]:Ft,[Ft]:Bn,[gn]:gn},d=new jt({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new Z},radius:{value:4}},vertexShader:N0,fragmentShader:O0}),f=d.clone();f.defines.HORIZONTAL_PASS=1;let m=new Ge;m.setAttribute("position",new et(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));let _=new Mt(m,d),g=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=cd;let p=this.type;this.render=function(b,w,R){if(g.enabled===!1||g.autoUpdate===!1&&g.needsUpdate===!1||b.length===0)return;let I=s.getRenderTarget(),M=s.getActiveCubeFace(),T=s.getActiveMipmapLevel(),O=s.state;O.setBlending(Dn),O.buffers.color.setClear(1,1,1,1),O.buffers.depth.setTest(!0),O.setScissorTest(!1);let Y=p!==pn&&this.type===pn,$=p===pn&&this.type!==pn;for(let U=0,z=b.length;Uh||i.y>h)&&(i.x>h&&(r.x=Math.floor(h/ne.x),i.x=r.x*ne.x,H.mapSize.x=r.x),i.y>h&&(r.y=Math.floor(h/ne.y),i.y=r.y*ne.y,H.mapSize.y=r.y)),H.map===null||Y===!0||$===!0){let K=this.type!==pn?{minFilter:pt,magFilter:pt}:{};H.map!==null&&H.map.dispose(),H.map=new qt(i.x,i.y,K),H.map.texture.name=q.name+".shadowMap",H.camera.updateProjectionMatrix()}s.setRenderTarget(H.map),s.clear();let W=H.getViewportCount();for(let K=0;K0||w.map&&w.alphaTest>0){let O=M.uuid,Y=w.uuid,$=l[O];$===void 0&&($={},l[O]=$);let U=$[Y];U===void 0&&(U=M.clone(),$[Y]=U),M=U}if(M.visible=w.visible,M.wireframe=w.wireframe,I===pn?M.side=w.shadowSide!==null?w.shadowSide:w.side:M.side=w.shadowSide!==null?w.shadowSide:u[w.side],M.alphaMap=w.alphaMap,M.alphaTest=w.alphaTest,M.map=w.map,M.clipShadows=w.clipShadows,M.clippingPlanes=w.clippingPlanes,M.clipIntersection=w.clipIntersection,M.displacementMap=w.displacementMap,M.displacementScale=w.displacementScale,M.displacementBias=w.displacementBias,M.wireframeLinewidth=w.wireframeLinewidth,M.linewidth=w.linewidth,R.isPointLight===!0&&M.isMeshDistanceMaterial===!0){let O=s.properties.get(M);O.light=R}return M}function y(b,w,R,I,M){if(b.visible===!1)return;if(b.layers.test(w.layers)&&(b.isMesh||b.isLine||b.isPoints)&&(b.castShadow||b.receiveShadow&&M===pn)&&(!b.frustumCulled||n.intersectsObject(b))){b.modelViewMatrix.multiplyMatrices(R.matrixWorldInverse,b.matrixWorld);let Y=e.update(b),$=b.material;if(Array.isArray($)){let U=Y.groups;for(let z=0,q=U.length;z=1):ne.indexOf("OpenGL ES")!==-1&&(H=parseFloat(/^OpenGL ES (\d)/.exec(ne)[1]),q=H>=2);let W=null,K={},D=s.getParameter(s.SCISSOR_BOX),G=s.getParameter(s.VIEWPORT),he=new je().fromArray(D),fe=new je().fromArray(G);function _e(P,ce,ae,ge){let ue=new Uint8Array(4),Q=s.createTexture();s.bindTexture(P,Q),s.texParameteri(P,s.TEXTURE_MIN_FILTER,s.NEAREST),s.texParameteri(P,s.TEXTURE_MAG_FILTER,s.NEAREST);for(let be=0;be"u"?!1:/OculusBrowser/g.test(navigator.userAgent),m=new WeakMap,_,g=new WeakMap,p=!1;try{p=typeof OffscreenCanvas<"u"&&new OffscreenCanvas(1,1).getContext("2d")!==null}catch{}function v(C,S){return p?new OffscreenCanvas(C,S):ws("canvas")}function x(C,S,B,ee){let j=1;if((C.width>ee||C.height>ee)&&(j=ee/Math.max(C.width,C.height)),j<1||S===!0)if(typeof HTMLImageElement<"u"&&C instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&C instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&C instanceof ImageBitmap){let te=S?Wr:Math.floor,Me=te(j*C.width),re=te(j*C.height);_===void 0&&(_=v(Me,re));let de=B?v(Me,re):_;return de.width=Me,de.height=re,de.getContext("2d").drawImage(C,0,0,Me,re),console.warn("THREE.WebGLRenderer: Texture has been resized from ("+C.width+"x"+C.height+") to ("+Me+"x"+re+")."),de}else return"data"in C&&console.warn("THREE.WebGLRenderer: Image in DataTexture is too big ("+C.width+"x"+C.height+")."),C;return C}function y(C){return mo(C.width)&&mo(C.height)}function b(C){return o?!1:C.wrapS!==It||C.wrapT!==It||C.minFilter!==pt&&C.minFilter!==mt}function w(C,S){return C.generateMipmaps&&S&&C.minFilter!==pt&&C.minFilter!==mt}function R(C){s.generateMipmap(C)}function I(C,S,B,ee,j=!1){if(o===!1)return S;if(C!==null){if(s[C]!==void 0)return s[C];console.warn("THREE.WebGLRenderer: Attempt to use non-existing WebGL internal format '"+C+"'")}let te=S;if(S===s.RED&&(B===s.FLOAT&&(te=s.R32F),B===s.HALF_FLOAT&&(te=s.R16F),B===s.UNSIGNED_BYTE&&(te=s.R8)),S===s.RED_INTEGER&&(B===s.UNSIGNED_BYTE&&(te=s.R8UI),B===s.UNSIGNED_SHORT&&(te=s.R16UI),B===s.UNSIGNED_INT&&(te=s.R32UI),B===s.BYTE&&(te=s.R8I),B===s.SHORT&&(te=s.R16I),B===s.INT&&(te=s.R32I)),S===s.RG&&(B===s.FLOAT&&(te=s.RG32F),B===s.HALF_FLOAT&&(te=s.RG16F),B===s.UNSIGNED_BYTE&&(te=s.RG8)),S===s.RGBA){let Me=j?zr:Qe.getTransfer(ee);B===s.FLOAT&&(te=s.RGBA32F),B===s.HALF_FLOAT&&(te=s.RGBA16F),B===s.UNSIGNED_BYTE&&(te=Me===nt?s.SRGB8_ALPHA8:s.RGBA8),B===s.UNSIGNED_SHORT_4_4_4_4&&(te=s.RGBA4),B===s.UNSIGNED_SHORT_5_5_5_1&&(te=s.RGB5_A1)}return(te===s.R16F||te===s.R32F||te===s.RG16F||te===s.RG32F||te===s.RGBA16F||te===s.RGBA32F)&&e.get("EXT_color_buffer_float"),te}function M(C,S,B){return w(C,B)===!0||C.isFramebufferTexture&&C.minFilter!==pt&&C.minFilter!==mt?Math.log2(Math.max(S.width,S.height))+1:C.mipmaps!==void 0&&C.mipmaps.length>0?C.mipmaps.length:C.isCompressedTexture&&Array.isArray(C.image)?S.mipmaps.length:1}function T(C){return C===pt||C===fo||C===Lr?s.NEAREST:s.LINEAR}function O(C){let S=C.target;S.removeEventListener("dispose",O),$(S),S.isVideoTexture&&m.delete(S)}function Y(C){let S=C.target;S.removeEventListener("dispose",Y),z(S)}function $(C){let S=n.get(C);if(S.__webglInit===void 0)return;let B=C.source,ee=g.get(B);if(ee){let j=ee[S.__cacheKey];j.usedTimes--,j.usedTimes===0&&U(C),Object.keys(ee).length===0&&g.delete(B)}n.remove(C)}function U(C){let S=n.get(C);s.deleteTexture(S.__webglTexture);let B=C.source,ee=g.get(B);delete ee[S.__cacheKey],a.memory.textures--}function z(C){let S=C.texture,B=n.get(C),ee=n.get(S);if(ee.__webglTexture!==void 0&&(s.deleteTexture(ee.__webglTexture),a.memory.textures--),C.depthTexture&&C.depthTexture.dispose(),C.isWebGLCubeRenderTarget)for(let j=0;j<6;j++){if(Array.isArray(B.__webglFramebuffer[j]))for(let te=0;te=c&&console.warn("THREE.WebGLTextures: Trying to use "+C+" texture units while this GPU supports only "+c),q+=1,C}function W(C){let S=[];return S.push(C.wrapS),S.push(C.wrapT),S.push(C.wrapR||0),S.push(C.magFilter),S.push(C.minFilter),S.push(C.anisotropy),S.push(C.internalFormat),S.push(C.format),S.push(C.type),S.push(C.generateMipmaps),S.push(C.premultiplyAlpha),S.push(C.flipY),S.push(C.unpackAlignment),S.push(C.colorSpace),S.join()}function K(C,S){let B=n.get(C);if(C.isVideoTexture&&qe(C),C.isRenderTargetTexture===!1&&C.version>0&&B.__version!==C.version){let ee=C.image;if(ee===null)console.warn("THREE.WebGLRenderer: Texture marked for update but no image data found.");else if(ee.complete===!1)console.warn("THREE.WebGLRenderer: Texture marked for update but image is incomplete");else{Ye(B,C,S);return}}t.bindTexture(s.TEXTURE_2D,B.__webglTexture,s.TEXTURE0+S)}function D(C,S){let B=n.get(C);if(C.version>0&&B.__version!==C.version){Ye(B,C,S);return}t.bindTexture(s.TEXTURE_2D_ARRAY,B.__webglTexture,s.TEXTURE0+S)}function G(C,S){let B=n.get(C);if(C.version>0&&B.__version!==C.version){Ye(B,C,S);return}t.bindTexture(s.TEXTURE_3D,B.__webglTexture,s.TEXTURE0+S)}function he(C,S){let B=n.get(C);if(C.version>0&&B.__version!==C.version){it(B,C,S);return}t.bindTexture(s.TEXTURE_CUBE_MAP,B.__webglTexture,s.TEXTURE0+S)}let fe={[Dr]:s.REPEAT,[It]:s.CLAMP_TO_EDGE,[Nr]:s.MIRRORED_REPEAT},_e={[pt]:s.NEAREST,[fo]:s.NEAREST_MIPMAP_NEAREST,[Lr]:s.NEAREST_MIPMAP_LINEAR,[mt]:s.LINEAR,[ud]:s.LINEAR_MIPMAP_NEAREST,[li]:s.LINEAR_MIPMAP_LINEAR},we={[Uf]:s.NEVER,[Vf]:s.ALWAYS,[Df]:s.LESS,[Of]:s.LEQUAL,[Nf]:s.EQUAL,[zf]:s.GEQUAL,[Ff]:s.GREATER,[Bf]:s.NOTEQUAL};function Ee(C,S,B){if(B?(s.texParameteri(C,s.TEXTURE_WRAP_S,fe[S.wrapS]),s.texParameteri(C,s.TEXTURE_WRAP_T,fe[S.wrapT]),(C===s.TEXTURE_3D||C===s.TEXTURE_2D_ARRAY)&&s.texParameteri(C,s.TEXTURE_WRAP_R,fe[S.wrapR]),s.texParameteri(C,s.TEXTURE_MAG_FILTER,_e[S.magFilter]),s.texParameteri(C,s.TEXTURE_MIN_FILTER,_e[S.minFilter])):(s.texParameteri(C,s.TEXTURE_WRAP_S,s.CLAMP_TO_EDGE),s.texParameteri(C,s.TEXTURE_WRAP_T,s.CLAMP_TO_EDGE),(C===s.TEXTURE_3D||C===s.TEXTURE_2D_ARRAY)&&s.texParameteri(C,s.TEXTURE_WRAP_R,s.CLAMP_TO_EDGE),(S.wrapS!==It||S.wrapT!==It)&&console.warn("THREE.WebGLRenderer: Texture is not power of two. Texture.wrapS and Texture.wrapT should be set to THREE.ClampToEdgeWrapping."),s.texParameteri(C,s.TEXTURE_MAG_FILTER,T(S.magFilter)),s.texParameteri(C,s.TEXTURE_MIN_FILTER,T(S.minFilter)),S.minFilter!==pt&&S.minFilter!==mt&&console.warn("THREE.WebGLRenderer: Texture is not power of two. Texture.minFilter should be set to THREE.NearestFilter or THREE.LinearFilter.")),S.compareFunction&&(s.texParameteri(C,s.TEXTURE_COMPARE_MODE,s.COMPARE_REF_TO_TEXTURE),s.texParameteri(C,s.TEXTURE_COMPARE_FUNC,we[S.compareFunction])),e.has("EXT_texture_filter_anisotropic")===!0){let ee=e.get("EXT_texture_filter_anisotropic");if(S.magFilter===pt||S.minFilter!==Lr&&S.minFilter!==li||S.type===xn&&e.has("OES_texture_float_linear")===!1||o===!1&&S.type===Ts&&e.has("OES_texture_half_float_linear")===!1)return;(S.anisotropy>1||n.get(S).__currentAnisotropy)&&(s.texParameterf(C,ee.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(S.anisotropy,i.getMaxAnisotropy())),n.get(S).__currentAnisotropy=S.anisotropy)}}function Te(C,S){let B=!1;C.__webglInit===void 0&&(C.__webglInit=!0,S.addEventListener("dispose",O));let ee=S.source,j=g.get(ee);j===void 0&&(j={},g.set(ee,j));let te=W(S);if(te!==C.__cacheKey){j[te]===void 0&&(j[te]={texture:s.createTexture(),usedTimes:0},a.memory.textures++,B=!0),j[te].usedTimes++;let Me=j[C.__cacheKey];Me!==void 0&&(j[C.__cacheKey].usedTimes--,Me.usedTimes===0&&U(S)),C.__cacheKey=te,C.__webglTexture=j[te].texture}return B}function Ye(C,S,B){let ee=s.TEXTURE_2D;(S.isDataArrayTexture||S.isCompressedArrayTexture)&&(ee=s.TEXTURE_2D_ARRAY),S.isData3DTexture&&(ee=s.TEXTURE_3D);let j=Te(C,S),te=S.source;t.bindTexture(ee,C.__webglTexture,s.TEXTURE0+B);let Me=n.get(te);if(te.version!==Me.__version||j===!0){t.activeTexture(s.TEXTURE0+B);let re=Qe.getPrimaries(Qe.workingColorSpace),de=S.colorSpace===Xt?null:Qe.getPrimaries(S.colorSpace),Le=S.colorSpace===Xt||re===de?s.NONE:s.BROWSER_DEFAULT_WEBGL;s.pixelStorei(s.UNPACK_FLIP_Y_WEBGL,S.flipY),s.pixelStorei(s.UNPACK_PREMULTIPLY_ALPHA_WEBGL,S.premultiplyAlpha),s.pixelStorei(s.UNPACK_ALIGNMENT,S.unpackAlignment),s.pixelStorei(s.UNPACK_COLORSPACE_CONVERSION_WEBGL,Le);let Ze=b(S)&&y(S.image)===!1,se=x(S.image,Ze,!1,h);se=rt(S,se);let $e=y(se)||o,Oe=r.convert(S.format,S.colorSpace),Ie=r.convert(S.type),Re=I(S.internalFormat,Oe,Ie,S.colorSpace,S.isVideoTexture);Ee(ee,S,$e);let P,ce=S.mipmaps,ae=o&&S.isVideoTexture!==!0,ge=Me.__version===void 0||j===!0,ue=M(S,se,$e);if(S.isDepthTexture)Re=s.DEPTH_COMPONENT,o?S.type===xn?Re=s.DEPTH_COMPONENT32F:S.type===Ln?Re=s.DEPTH_COMPONENT24:S.type===ii?Re=s.DEPTH24_STENCIL8:Re=s.DEPTH_COMPONENT16:S.type===xn&&console.error("WebGLRenderer: Floating point depth texture requires WebGL2."),S.format===si&&Re===s.DEPTH_COMPONENT&&S.type!==Wc&&S.type!==Ln&&(console.warn("THREE.WebGLRenderer: Use UnsignedShortType or UnsignedIntType for DepthFormat DepthTexture."),S.type=Ln,Ie=r.convert(S.type)),S.format===Yi&&Re===s.DEPTH_COMPONENT&&(Re=s.DEPTH_STENCIL,S.type!==ii&&(console.warn("THREE.WebGLRenderer: Use UnsignedInt248Type for DepthStencilFormat DepthTexture."),S.type=ii,Ie=r.convert(S.type))),ge&&(ae?t.texStorage2D(s.TEXTURE_2D,1,Re,se.width,se.height):t.texImage2D(s.TEXTURE_2D,0,Re,se.width,se.height,0,Oe,Ie,null));else if(S.isDataTexture)if(ce.length>0&&$e){ae&&ge&&t.texStorage2D(s.TEXTURE_2D,ue,Re,ce[0].width,ce[0].height);for(let Q=0,be=ce.length;Q>=1,be>>=1}}else if(ce.length>0&&$e){ae&&ge&&t.texStorage2D(s.TEXTURE_2D,ue,Re,ce[0].width,ce[0].height);for(let Q=0,be=ce.length;Q0&&ge++,t.texStorage2D(s.TEXTURE_CUBE_MAP,ge,P,se[0].width,se[0].height));for(let Q=0;Q<6;Q++)if(Ze){ce?t.texSubImage2D(s.TEXTURE_CUBE_MAP_POSITIVE_X+Q,0,0,0,se[Q].width,se[Q].height,Ie,Re,se[Q].data):t.texImage2D(s.TEXTURE_CUBE_MAP_POSITIVE_X+Q,0,P,se[Q].width,se[Q].height,0,Ie,Re,se[Q].data);for(let be=0;be>te),se=Math.max(1,S.height>>te);j===s.TEXTURE_3D||j===s.TEXTURE_2D_ARRAY?t.texImage3D(j,te,de,Ze,se,S.depth,0,Me,re,null):t.texImage2D(j,te,de,Ze,se,0,Me,re,null)}t.bindFramebuffer(s.FRAMEBUFFER,C),Ne(S)?d.framebufferTexture2DMultisampleEXT(s.FRAMEBUFFER,ee,j,n.get(B).__webglTexture,0,ye(S)):(j===s.TEXTURE_2D||j>=s.TEXTURE_CUBE_MAP_POSITIVE_X&&j<=s.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&s.framebufferTexture2D(s.FRAMEBUFFER,ee,j,n.get(B).__webglTexture,te),t.bindFramebuffer(s.FRAMEBUFFER,null)}function L(C,S,B){if(s.bindRenderbuffer(s.RENDERBUFFER,C),S.depthBuffer&&!S.stencilBuffer){let ee=o===!0?s.DEPTH_COMPONENT24:s.DEPTH_COMPONENT16;if(B||Ne(S)){let j=S.depthTexture;j&&j.isDepthTexture&&(j.type===xn?ee=s.DEPTH_COMPONENT32F:j.type===Ln&&(ee=s.DEPTH_COMPONENT24));let te=ye(S);Ne(S)?d.renderbufferStorageMultisampleEXT(s.RENDERBUFFER,te,ee,S.width,S.height):s.renderbufferStorageMultisample(s.RENDERBUFFER,te,ee,S.width,S.height)}else s.renderbufferStorage(s.RENDERBUFFER,ee,S.width,S.height);s.framebufferRenderbuffer(s.FRAMEBUFFER,s.DEPTH_ATTACHMENT,s.RENDERBUFFER,C)}else if(S.depthBuffer&&S.stencilBuffer){let ee=ye(S);B&&Ne(S)===!1?s.renderbufferStorageMultisample(s.RENDERBUFFER,ee,s.DEPTH24_STENCIL8,S.width,S.height):Ne(S)?d.renderbufferStorageMultisampleEXT(s.RENDERBUFFER,ee,s.DEPTH24_STENCIL8,S.width,S.height):s.renderbufferStorage(s.RENDERBUFFER,s.DEPTH_STENCIL,S.width,S.height),s.framebufferRenderbuffer(s.FRAMEBUFFER,s.DEPTH_STENCIL_ATTACHMENT,s.RENDERBUFFER,C)}else{let ee=S.isWebGLMultipleRenderTargets===!0?S.texture:[S.texture];for(let j=0;j0){B.__webglFramebuffer[re]=[];for(let de=0;de0){B.__webglFramebuffer=[];for(let re=0;re0&&Ne(C)===!1){let re=te?S:[S];B.__webglMultisampledFramebuffer=s.createFramebuffer(),B.__webglColorRenderbuffer=[],t.bindFramebuffer(s.FRAMEBUFFER,B.__webglMultisampledFramebuffer);for(let de=0;de0)for(let de=0;de0)for(let de=0;de0&&Ne(C)===!1){let S=C.isWebGLMultipleRenderTargets?C.texture:[C.texture],B=C.width,ee=C.height,j=s.COLOR_BUFFER_BIT,te=[],Me=C.stencilBuffer?s.DEPTH_STENCIL_ATTACHMENT:s.DEPTH_ATTACHMENT,re=n.get(C),de=C.isWebGLMultipleRenderTargets===!0;if(de)for(let Le=0;Le0&&e.has("WEBGL_multisampled_render_to_texture")===!0&&S.__useRenderToTexture!==!1}function qe(C){let S=a.render.frame;m.get(C)!==S&&(m.set(C,S),C.update())}function rt(C,S){let B=C.colorSpace,ee=C.format,j=C.type;return C.isCompressedTexture===!0||C.isVideoTexture===!0||C.format===po||B!==Mn&&B!==Xt&&(Qe.getTransfer(B)===nt?o===!1?e.has("EXT_sRGB")===!0&&ee===Wt?(C.format=po,C.minFilter=mt,C.generateMipmaps=!1):S=Xr.sRGBToLinear(S):(ee!==Wt||j!==On)&&console.warn("THREE.WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):console.error("THREE.WebGLTextures: Unsupported texture color space:",B)),S}this.allocateTextureUnit=ne,this.resetTextureUnits=H,this.setTexture2D=K,this.setTexture2DArray=D,this.setTexture3D=G,this.setTextureCube=he,this.rebindTextures=ie,this.setupRenderTarget=J,this.updateRenderTargetMipmap=Se,this.updateMultisampleRenderTarget=me,this.setupDepthRenderbuffer=X,this.setupFrameBufferTexture=Ce,this.useMultisampledRTT=Ne}function V0(s,e,t){let n=t.isWebGL2;function i(r,a=Xt){let o,c=Qe.getTransfer(a);if(r===On)return s.UNSIGNED_BYTE;if(r===fd)return s.UNSIGNED_SHORT_4_4_4_4;if(r===pd)return s.UNSIGNED_SHORT_5_5_5_1;if(r===_f)return s.BYTE;if(r===xf)return s.SHORT;if(r===Wc)return s.UNSIGNED_SHORT;if(r===dd)return s.INT;if(r===Ln)return s.UNSIGNED_INT;if(r===xn)return s.FLOAT;if(r===Ts)return n?s.HALF_FLOAT:(o=e.get("OES_texture_half_float"),o!==null?o.HALF_FLOAT_OES:null);if(r===vf)return s.ALPHA;if(r===Wt)return s.RGBA;if(r===yf)return s.LUMINANCE;if(r===Mf)return s.LUMINANCE_ALPHA;if(r===si)return s.DEPTH_COMPONENT;if(r===Yi)return s.DEPTH_STENCIL;if(r===po)return o=e.get("EXT_sRGB"),o!==null?o.SRGB_ALPHA_EXT:null;if(r===Sf)return s.RED;if(r===md)return s.RED_INTEGER;if(r===bf)return s.RG;if(r===gd)return s.RG_INTEGER;if(r===_d)return s.RGBA_INTEGER;if(r===wa||r===Aa||r===Ra||r===Ca)if(c===nt)if(o=e.get("WEBGL_compressed_texture_s3tc_srgb"),o!==null){if(r===wa)return o.COMPRESSED_SRGB_S3TC_DXT1_EXT;if(r===Aa)return o.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;if(r===Ra)return o.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;if(r===Ca)return o.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT}else return null;else if(o=e.get("WEBGL_compressed_texture_s3tc"),o!==null){if(r===wa)return o.COMPRESSED_RGB_S3TC_DXT1_EXT;if(r===Aa)return o.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(r===Ra)return o.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(r===Ca)return o.COMPRESSED_RGBA_S3TC_DXT5_EXT}else return null;if(r===ul||r===dl||r===fl||r===pl)if(o=e.get("WEBGL_compressed_texture_pvrtc"),o!==null){if(r===ul)return o.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(r===dl)return o.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(r===fl)return o.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(r===pl)return o.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}else return null;if(r===Ef)return o=e.get("WEBGL_compressed_texture_etc1"),o!==null?o.COMPRESSED_RGB_ETC1_WEBGL:null;if(r===ml||r===gl)if(o=e.get("WEBGL_compressed_texture_etc"),o!==null){if(r===ml)return c===nt?o.COMPRESSED_SRGB8_ETC2:o.COMPRESSED_RGB8_ETC2;if(r===gl)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:o.COMPRESSED_RGBA8_ETC2_EAC}else return null;if(r===_l||r===xl||r===vl||r===yl||r===Ml||r===Sl||r===bl||r===El||r===Tl||r===wl||r===Al||r===Rl||r===Cl||r===Pl)if(o=e.get("WEBGL_compressed_texture_astc"),o!==null){if(r===_l)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR:o.COMPRESSED_RGBA_ASTC_4x4_KHR;if(r===xl)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR:o.COMPRESSED_RGBA_ASTC_5x4_KHR;if(r===vl)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR:o.COMPRESSED_RGBA_ASTC_5x5_KHR;if(r===yl)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR:o.COMPRESSED_RGBA_ASTC_6x5_KHR;if(r===Ml)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR:o.COMPRESSED_RGBA_ASTC_6x6_KHR;if(r===Sl)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR:o.COMPRESSED_RGBA_ASTC_8x5_KHR;if(r===bl)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR:o.COMPRESSED_RGBA_ASTC_8x6_KHR;if(r===El)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR:o.COMPRESSED_RGBA_ASTC_8x8_KHR;if(r===Tl)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR:o.COMPRESSED_RGBA_ASTC_10x5_KHR;if(r===wl)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR:o.COMPRESSED_RGBA_ASTC_10x6_KHR;if(r===Al)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR:o.COMPRESSED_RGBA_ASTC_10x8_KHR;if(r===Rl)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR:o.COMPRESSED_RGBA_ASTC_10x10_KHR;if(r===Cl)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR:o.COMPRESSED_RGBA_ASTC_12x10_KHR;if(r===Pl)return c===nt?o.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR:o.COMPRESSED_RGBA_ASTC_12x12_KHR}else return null;if(r===Pa||r===Ll||r===Il)if(o=e.get("EXT_texture_compression_bptc"),o!==null){if(r===Pa)return c===nt?o.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT:o.COMPRESSED_RGBA_BPTC_UNORM_EXT;if(r===Ll)return o.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;if(r===Il)return o.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT}else return null;if(r===Tf||r===Ul||r===Dl||r===Nl)if(o=e.get("EXT_texture_compression_rgtc"),o!==null){if(r===Pa)return o.COMPRESSED_RED_RGTC1_EXT;if(r===Ul)return o.COMPRESSED_SIGNED_RED_RGTC1_EXT;if(r===Dl)return o.COMPRESSED_RED_GREEN_RGTC2_EXT;if(r===Nl)return o.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT}else return null;return r===ii?n?s.UNSIGNED_INT_24_8:(o=e.get("WEBGL_depth_texture"),o!==null?o.UNSIGNED_INT_24_8_WEBGL:null):s[r]!==void 0?s[r]:null}return{convert:i}}var To=class extends yt{constructor(e=[]){super(),this.isArrayCamera=!0,this.cameras=e}},ti=class extends Je{constructor(){super(),this.isGroup=!0,this.type="Group"}},k0={type:"move"},Ss=class{constructor(){this._targetRay=null,this._grip=null,this._hand=null}getHandSpace(){return this._hand===null&&(this._hand=new ti,this._hand.matrixAutoUpdate=!1,this._hand.visible=!1,this._hand.joints={},this._hand.inputState={pinching:!1}),this._hand}getTargetRaySpace(){return this._targetRay===null&&(this._targetRay=new ti,this._targetRay.matrixAutoUpdate=!1,this._targetRay.visible=!1,this._targetRay.hasLinearVelocity=!1,this._targetRay.linearVelocity=new A,this._targetRay.hasAngularVelocity=!1,this._targetRay.angularVelocity=new A),this._targetRay}getGripSpace(){return this._grip===null&&(this._grip=new ti,this._grip.matrixAutoUpdate=!1,this._grip.visible=!1,this._grip.hasLinearVelocity=!1,this._grip.linearVelocity=new A,this._grip.hasAngularVelocity=!1,this._grip.angularVelocity=new A),this._grip}dispatchEvent(e){return this._targetRay!==null&&this._targetRay.dispatchEvent(e),this._grip!==null&&this._grip.dispatchEvent(e),this._hand!==null&&this._hand.dispatchEvent(e),this}connect(e){if(e&&e.hand){let t=this._hand;if(t)for(let n of e.hand.values())this._getHandJoint(t,n)}return this.dispatchEvent({type:"connected",data:e}),this}disconnect(e){return this.dispatchEvent({type:"disconnected",data:e}),this._targetRay!==null&&(this._targetRay.visible=!1),this._grip!==null&&(this._grip.visible=!1),this._hand!==null&&(this._hand.visible=!1),this}update(e,t,n){let i=null,r=null,a=null,o=this._targetRay,c=this._grip,l=this._hand;if(e&&t.session.visibilityState!=="visible-blurred"){if(l&&e.hand){a=!0;for(let _ of e.hand.values()){let g=t.getJointPose(_,n),p=this._getHandJoint(l,_);g!==null&&(p.matrix.fromArray(g.transform.matrix),p.matrix.decompose(p.position,p.rotation,p.scale),p.matrixWorldNeedsUpdate=!0,p.jointRadius=g.radius),p.visible=g!==null}let h=l.joints["index-finger-tip"],u=l.joints["thumb-tip"],d=h.position.distanceTo(u.position),f=.02,m=.005;l.inputState.pinching&&d>f+m?(l.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:e.handedness,target:this})):!l.inputState.pinching&&d<=f-m&&(l.inputState.pinching=!0,this.dispatchEvent({type:"pinchstart",handedness:e.handedness,target:this}))}else c!==null&&e.gripSpace&&(r=t.getPose(e.gripSpace,n),r!==null&&(c.matrix.fromArray(r.transform.matrix),c.matrix.decompose(c.position,c.rotation,c.scale),c.matrixWorldNeedsUpdate=!0,r.linearVelocity?(c.hasLinearVelocity=!0,c.linearVelocity.copy(r.linearVelocity)):c.hasLinearVelocity=!1,r.angularVelocity?(c.hasAngularVelocity=!0,c.angularVelocity.copy(r.angularVelocity)):c.hasAngularVelocity=!1));o!==null&&(i=t.getPose(e.targetRaySpace,n),i===null&&r!==null&&(i=r),i!==null&&(o.matrix.fromArray(i.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),o.matrixWorldNeedsUpdate=!0,i.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(i.linearVelocity)):o.hasLinearVelocity=!1,i.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(i.angularVelocity)):o.hasAngularVelocity=!1,this.dispatchEvent(k0)))}return o!==null&&(o.visible=i!==null),c!==null&&(c.visible=r!==null),l!==null&&(l.visible=a!==null),this}_getHandJoint(e,t){if(e.joints[t.jointName]===void 0){let n=new ti;n.matrixAutoUpdate=!1,n.visible=!1,e.joints[t.jointName]=n,e.add(n)}return e.joints[t.jointName]}},wo=class extends St{constructor(e,t,n,i,r,a,o,c,l,h){if(h=h!==void 0?h:si,h!==si&&h!==Yi)throw new Error("DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat");n===void 0&&h===si&&(n=Ln),n===void 0&&h===Yi&&(n=ii),super(null,i,r,a,o,c,h,n,l),this.isDepthTexture=!0,this.image={width:e,height:t},this.magFilter=o!==void 0?o:pt,this.minFilter=c!==void 0?c:pt,this.flipY=!1,this.generateMipmaps=!1,this.compareFunction=null}copy(e){return super.copy(e),this.compareFunction=e.compareFunction,this}toJSON(e){let t=super.toJSON(e);return this.compareFunction!==null&&(t.compareFunction=this.compareFunction),t}},Ao=class extends sn{constructor(e,t){super();let n=this,i=null,r=1,a=null,o="local-floor",c=1,l=null,h=null,u=null,d=null,f=null,m=null,_=t.getContextAttributes(),g=null,p=null,v=[],x=[],y=new yt;y.layers.enable(1),y.viewport=new je;let b=new yt;b.layers.enable(2),b.viewport=new je;let w=[y,b],R=new To;R.layers.enable(1),R.layers.enable(2);let I=null,M=null;this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function(D){let G=v[D];return G===void 0&&(G=new Ss,v[D]=G),G.getTargetRaySpace()},this.getControllerGrip=function(D){let G=v[D];return G===void 0&&(G=new Ss,v[D]=G),G.getGripSpace()},this.getHand=function(D){let G=v[D];return G===void 0&&(G=new Ss,v[D]=G),G.getHandSpace()};function T(D){let G=x.indexOf(D.inputSource);if(G===-1)return;let he=v[G];he!==void 0&&(he.update(D.inputSource,D.frame,l||a),he.dispatchEvent({type:D.type,data:D.inputSource}))}function O(){i.removeEventListener("select",T),i.removeEventListener("selectstart",T),i.removeEventListener("selectend",T),i.removeEventListener("squeeze",T),i.removeEventListener("squeezestart",T),i.removeEventListener("squeezeend",T),i.removeEventListener("end",O),i.removeEventListener("inputsourceschange",Y);for(let D=0;D=0&&(x[fe]=null,v[fe].disconnect(he))}for(let G=0;G=x.length){x.push(he),fe=we;break}else if(x[we]===null){x[we]=he,fe=we;break}if(fe===-1)break}let _e=v[fe];_e&&_e.connect(he)}}let $=new A,U=new A;function z(D,G,he){$.setFromMatrixPosition(G.matrixWorld),U.setFromMatrixPosition(he.matrixWorld);let fe=$.distanceTo(U),_e=G.projectionMatrix.elements,we=he.projectionMatrix.elements,Ee=_e[14]/(_e[10]-1),Te=_e[14]/(_e[10]+1),Ye=(_e[9]+1)/_e[5],it=(_e[9]-1)/_e[5],Ce=(_e[8]-1)/_e[0],L=(we[8]+1)/we[0],oe=Ee*Ce,X=Ee*L,ie=fe/(-Ce+L),J=ie*-Ce;G.matrixWorld.decompose(D.position,D.quaternion,D.scale),D.translateX(J),D.translateZ(ie),D.matrixWorld.compose(D.position,D.quaternion,D.scale),D.matrixWorldInverse.copy(D.matrixWorld).invert();let Se=Ee+ie,me=Te+ie,ye=oe-J,Ne=X+(fe-J),qe=Ye*Te/me*Se,rt=it*Te/me*Se;D.projectionMatrix.makePerspective(ye,Ne,qe,rt,Se,me),D.projectionMatrixInverse.copy(D.projectionMatrix).invert()}function q(D,G){G===null?D.matrixWorld.copy(D.matrix):D.matrixWorld.multiplyMatrices(G.matrixWorld,D.matrix),D.matrixWorldInverse.copy(D.matrixWorld).invert()}this.updateCamera=function(D){if(i===null)return;R.near=b.near=y.near=D.near,R.far=b.far=y.far=D.far,(I!==R.near||M!==R.far)&&(i.updateRenderState({depthNear:R.near,depthFar:R.far}),I=R.near,M=R.far);let G=D.parent,he=R.cameras;q(R,G);for(let fe=0;fe0&&(g.alphaTest.value=p.alphaTest);let v=e.get(p).envMap;if(v&&(g.envMap.value=v,g.flipEnvMap.value=v.isCubeTexture&&v.isRenderTargetTexture===!1?-1:1,g.reflectivity.value=p.reflectivity,g.ior.value=p.ior,g.refractionRatio.value=p.refractionRatio),p.lightMap){g.lightMap.value=p.lightMap;let x=s._useLegacyLights===!0?Math.PI:1;g.lightMapIntensity.value=p.lightMapIntensity*x,t(p.lightMap,g.lightMapTransform)}p.aoMap&&(g.aoMap.value=p.aoMap,g.aoMapIntensity.value=p.aoMapIntensity,t(p.aoMap,g.aoMapTransform))}function a(g,p){g.diffuse.value.copy(p.color),g.opacity.value=p.opacity,p.map&&(g.map.value=p.map,t(p.map,g.mapTransform))}function o(g,p){g.dashSize.value=p.dashSize,g.totalSize.value=p.dashSize+p.gapSize,g.scale.value=p.scale}function c(g,p,v,x){g.diffuse.value.copy(p.color),g.opacity.value=p.opacity,g.size.value=p.size*v,g.scale.value=x*.5,p.map&&(g.map.value=p.map,t(p.map,g.uvTransform)),p.alphaMap&&(g.alphaMap.value=p.alphaMap,t(p.alphaMap,g.alphaMapTransform)),p.alphaTest>0&&(g.alphaTest.value=p.alphaTest)}function l(g,p){g.diffuse.value.copy(p.color),g.opacity.value=p.opacity,g.rotation.value=p.rotation,p.map&&(g.map.value=p.map,t(p.map,g.mapTransform)),p.alphaMap&&(g.alphaMap.value=p.alphaMap,t(p.alphaMap,g.alphaMapTransform)),p.alphaTest>0&&(g.alphaTest.value=p.alphaTest)}function h(g,p){g.specular.value.copy(p.specular),g.shininess.value=Math.max(p.shininess,1e-4)}function u(g,p){p.gradientMap&&(g.gradientMap.value=p.gradientMap)}function d(g,p){g.metalness.value=p.metalness,p.metalnessMap&&(g.metalnessMap.value=p.metalnessMap,t(p.metalnessMap,g.metalnessMapTransform)),g.roughness.value=p.roughness,p.roughnessMap&&(g.roughnessMap.value=p.roughnessMap,t(p.roughnessMap,g.roughnessMapTransform)),e.get(p).envMap&&(g.envMapIntensity.value=p.envMapIntensity)}function f(g,p,v){g.ior.value=p.ior,p.sheen>0&&(g.sheenColor.value.copy(p.sheenColor).multiplyScalar(p.sheen),g.sheenRoughness.value=p.sheenRoughness,p.sheenColorMap&&(g.sheenColorMap.value=p.sheenColorMap,t(p.sheenColorMap,g.sheenColorMapTransform)),p.sheenRoughnessMap&&(g.sheenRoughnessMap.value=p.sheenRoughnessMap,t(p.sheenRoughnessMap,g.sheenRoughnessMapTransform))),p.clearcoat>0&&(g.clearcoat.value=p.clearcoat,g.clearcoatRoughness.value=p.clearcoatRoughness,p.clearcoatMap&&(g.clearcoatMap.value=p.clearcoatMap,t(p.clearcoatMap,g.clearcoatMapTransform)),p.clearcoatRoughnessMap&&(g.clearcoatRoughnessMap.value=p.clearcoatRoughnessMap,t(p.clearcoatRoughnessMap,g.clearcoatRoughnessMapTransform)),p.clearcoatNormalMap&&(g.clearcoatNormalMap.value=p.clearcoatNormalMap,t(p.clearcoatNormalMap,g.clearcoatNormalMapTransform),g.clearcoatNormalScale.value.copy(p.clearcoatNormalScale),p.side===Ft&&g.clearcoatNormalScale.value.negate())),p.iridescence>0&&(g.iridescence.value=p.iridescence,g.iridescenceIOR.value=p.iridescenceIOR,g.iridescenceThicknessMinimum.value=p.iridescenceThicknessRange[0],g.iridescenceThicknessMaximum.value=p.iridescenceThicknessRange[1],p.iridescenceMap&&(g.iridescenceMap.value=p.iridescenceMap,t(p.iridescenceMap,g.iridescenceMapTransform)),p.iridescenceThicknessMap&&(g.iridescenceThicknessMap.value=p.iridescenceThicknessMap,t(p.iridescenceThicknessMap,g.iridescenceThicknessMapTransform))),p.transmission>0&&(g.transmission.value=p.transmission,g.transmissionSamplerMap.value=v.texture,g.transmissionSamplerSize.value.set(v.width,v.height),p.transmissionMap&&(g.transmissionMap.value=p.transmissionMap,t(p.transmissionMap,g.transmissionMapTransform)),g.thickness.value=p.thickness,p.thicknessMap&&(g.thicknessMap.value=p.thicknessMap,t(p.thicknessMap,g.thicknessMapTransform)),g.attenuationDistance.value=p.attenuationDistance,g.attenuationColor.value.copy(p.attenuationColor)),p.anisotropy>0&&(g.anisotropyVector.value.set(p.anisotropy*Math.cos(p.anisotropyRotation),p.anisotropy*Math.sin(p.anisotropyRotation)),p.anisotropyMap&&(g.anisotropyMap.value=p.anisotropyMap,t(p.anisotropyMap,g.anisotropyMapTransform))),g.specularIntensity.value=p.specularIntensity,g.specularColor.value.copy(p.specularColor),p.specularColorMap&&(g.specularColorMap.value=p.specularColorMap,t(p.specularColorMap,g.specularColorMapTransform)),p.specularIntensityMap&&(g.specularIntensityMap.value=p.specularIntensityMap,t(p.specularIntensityMap,g.specularIntensityMapTransform))}function m(g,p){p.matcap&&(g.matcap.value=p.matcap)}function _(g,p){let v=e.get(p).light;g.referencePosition.value.setFromMatrixPosition(v.matrixWorld),g.nearDistance.value=v.shadow.camera.near,g.farDistance.value=v.shadow.camera.far}return{refreshFogUniforms:n,refreshMaterialUniforms:i}}function G0(s,e,t,n){let i={},r={},a=[],o=t.isWebGL2?s.getParameter(s.MAX_UNIFORM_BUFFER_BINDINGS):0;function c(v,x){let y=x.program;n.uniformBlockBinding(v,y)}function l(v,x){let y=i[v.id];y===void 0&&(m(v),y=h(v),i[v.id]=y,v.addEventListener("dispose",g));let b=x.program;n.updateUBOMapping(v,b);let w=e.render.frame;r[v.id]!==w&&(d(v),r[v.id]=w)}function h(v){let x=u();v.__bindingPointIndex=x;let y=s.createBuffer(),b=v.__size,w=v.usage;return s.bindBuffer(s.UNIFORM_BUFFER,y),s.bufferData(s.UNIFORM_BUFFER,b,w),s.bindBuffer(s.UNIFORM_BUFFER,null),s.bindBufferBase(s.UNIFORM_BUFFER,x,y),y}function u(){for(let v=0;v0){w=y%b;let Y=b-w;w!==0&&Y-T.boundary<0&&(y+=b-w,M.__offset=y)}y+=T.storage}return w=y%b,w>0&&(y+=b-w),v.__size=y,v.__cache={},this}function _(v){let x={boundary:0,storage:0};return typeof v=="number"?(x.boundary=4,x.storage=4):v.isVector2?(x.boundary=8,x.storage=8):v.isVector3||v.isColor?(x.boundary=16,x.storage=12):v.isVector4?(x.boundary=16,x.storage=16):v.isMatrix3?(x.boundary=48,x.storage=48):v.isMatrix4?(x.boundary=64,x.storage=64):v.isTexture?console.warn("THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group."):console.warn("THREE.WebGLRenderer: Unsupported uniform value type.",v),x}function g(v){let x=v.target;x.removeEventListener("dispose",g);let y=a.indexOf(x.__bindingPointIndex);a.splice(y,1),s.deleteBuffer(i[x.id]),delete i[x.id],delete r[x.id]}function p(){for(let v in i)s.deleteBuffer(i[v]);a=[],i={},r={}}return{bind:c,update:l,dispose:p}}var Ro=class{constructor(e={}){let{canvas:t=tp(),context:n=null,depth:i=!0,stencil:r=!0,alpha:a=!1,antialias:o=!1,premultipliedAlpha:c=!0,preserveDrawingBuffer:l=!1,powerPreference:h="default",failIfMajorPerformanceCaveat:u=!1}=e;this.isWebGLRenderer=!0;let d;n!==null?d=n.getContextAttributes().alpha:d=a;let f=new Uint32Array(4),m=new Int32Array(4),_=null,g=null,p=[],v=[];this.domElement=t,this.debug={checkShaderErrors:!0,onShaderError:null},this.autoClear=!0,this.autoClearColor=!0,this.autoClearDepth=!0,this.autoClearStencil=!0,this.sortObjects=!0,this.clippingPlanes=[],this.localClippingEnabled=!1,this._outputColorSpace=vt,this._useLegacyLights=!1,this.toneMapping=Nn,this.toneMappingExposure=1;let x=this,y=!1,b=0,w=0,R=null,I=-1,M=null,T=new je,O=new je,Y=null,$=new pe(0),U=0,z=t.width,q=t.height,H=1,ne=null,W=null,K=new je(0,0,z,q),D=new je(0,0,z,q),G=!1,he=new Ps,fe=!1,_e=!1,we=null,Ee=new ze,Te=new Z,Ye=new A,it={background:null,fog:null,environment:null,overrideMaterial:null,isScene:!0};function Ce(){return R===null?H:1}let L=n;function oe(E,N){for(let V=0;V0?g=v[v.length-1]:g=null,p.pop(),p.length>0?_=p[p.length-1]:_=null};function jc(E,N,V,F){if(E.visible===!1)return;if(E.layers.test(N.layers)){if(E.isGroup)V=E.renderOrder;else if(E.isLOD)E.autoUpdate===!0&&E.update(N);else if(E.isLight)g.pushLight(E),E.castShadow&&g.pushShadow(E);else if(E.isSprite){if(!E.frustumCulled||he.intersectsSprite(E)){F&&Ye.setFromMatrixPosition(E.matrixWorld).applyMatrix4(Ee);let Ae=S.update(E),Ue=E.material;Ue.visible&&_.push(E,Ae,Ue,V,Ye.z,null)}}else if((E.isMesh||E.isLine||E.isPoints)&&(!E.frustumCulled||he.intersectsObject(E))){let Ae=S.update(E),Ue=E.material;if(F&&(E.boundingSphere!==void 0?(E.boundingSphere===null&&E.computeBoundingSphere(),Ye.copy(E.boundingSphere.center)):(Ae.boundingSphere===null&&Ae.computeBoundingSphere(),Ye.copy(Ae.boundingSphere.center)),Ye.applyMatrix4(E.matrixWorld).applyMatrix4(Ee)),Array.isArray(Ue)){let De=Ae.groups;for(let We=0,Pe=De.length;We0&&Od(k,xe,N,V),F&&J.viewport(T.copy(F)),k.length>0&&ks(k,N,V),xe.length>0&&ks(xe,N,V),Ae.length>0&&ks(Ae,N,V),J.buffers.depth.setTest(!0),J.buffers.depth.setMask(!0),J.buffers.color.setMask(!0),J.setPolygonOffset(!1)}function Od(E,N,V,F){let k=ie.isWebGL2;we===null&&(we=new qt(1,1,{generateMipmaps:!0,type:X.has("EXT_color_buffer_half_float")?Ts:On,minFilter:li,samples:k?4:0})),x.getDrawingBufferSize(Te),k?we.setSize(Te.x,Te.y):we.setSize(Wr(Te.x),Wr(Te.y));let xe=x.getRenderTarget();x.setRenderTarget(we),x.getClearColor($),U=x.getClearAlpha(),U<1&&x.setClearColor(16777215,.5),x.clear();let Ae=x.toneMapping;x.toneMapping=Nn,ks(E,V,F),ye.updateMultisampleRenderTarget(we),ye.updateRenderTargetMipmap(we);let Ue=!1;for(let De=0,We=N.length;De0),Ve=!!V.morphAttributes.position,at=!!V.morphAttributes.normal,lt=!!V.morphAttributes.color,Ht=Nn;F.toneMapped&&(R===null||R.isXRRenderTarget===!0)&&(Ht=x.toneMapping);let an=V.morphAttributes.position||V.morphAttributes.normal||V.morphAttributes.color,ut=an!==void 0?an.length:0,Xe=me.get(F),Sa=g.state.lights;if(fe===!0&&(_e===!0||E!==M)){let Bt=E===M&&F.id===I;Me.setState(F,E,Bt)}let dt=!1;F.version===Xe.__version?(Xe.needsLights&&Xe.lightsStateVersion!==Sa.state.version||Xe.outputColorSpace!==Ue||k.isInstancedMesh&&Xe.instancing===!1||!k.isInstancedMesh&&Xe.instancing===!0||k.isSkinnedMesh&&Xe.skinning===!1||!k.isSkinnedMesh&&Xe.skinning===!0||k.isInstancedMesh&&Xe.instancingColor===!0&&k.instanceColor===null||k.isInstancedMesh&&Xe.instancingColor===!1&&k.instanceColor!==null||Xe.envMap!==De||F.fog===!0&&Xe.fog!==xe||Xe.numClippingPlanes!==void 0&&(Xe.numClippingPlanes!==Me.numPlanes||Xe.numIntersection!==Me.numIntersection)||Xe.vertexAlphas!==We||Xe.vertexTangents!==Pe||Xe.morphTargets!==Ve||Xe.morphNormals!==at||Xe.morphColors!==lt||Xe.toneMapping!==Ht||ie.isWebGL2===!0&&Xe.morphTargetsCount!==ut)&&(dt=!0):(dt=!0,Xe.__version=F.version);let Hn=Xe.currentProgram;dt===!0&&(Hn=Hs(F,N,k));let il=!1,os=!1,ba=!1,Ct=Hn.getUniforms(),Gn=Xe.uniforms;if(J.useProgram(Hn.program)&&(il=!0,os=!0,ba=!0),F.id!==I&&(I=F.id,os=!0),il||M!==E){Ct.setValue(L,"projectionMatrix",E.projectionMatrix),Ct.setValue(L,"viewMatrix",E.matrixWorldInverse);let Bt=Ct.map.cameraPosition;Bt!==void 0&&Bt.setValue(L,Ye.setFromMatrixPosition(E.matrixWorld)),ie.logarithmicDepthBuffer&&Ct.setValue(L,"logDepthBufFC",2/(Math.log(E.far+1)/Math.LN2)),(F.isMeshPhongMaterial||F.isMeshToonMaterial||F.isMeshLambertMaterial||F.isMeshBasicMaterial||F.isMeshStandardMaterial||F.isShaderMaterial)&&Ct.setValue(L,"isOrthographic",E.isOrthographicCamera===!0),M!==E&&(M=E,os=!0,ba=!0)}if(k.isSkinnedMesh){Ct.setOptional(L,k,"bindMatrix"),Ct.setOptional(L,k,"bindMatrixInverse");let Bt=k.skeleton;Bt&&(ie.floatVertexTextures?(Bt.boneTexture===null&&Bt.computeBoneTexture(),Ct.setValue(L,"boneTexture",Bt.boneTexture,ye),Ct.setValue(L,"boneTextureSize",Bt.boneTextureSize)):console.warn("THREE.WebGLRenderer: SkinnedMesh can only be used with WebGL 2. With WebGL 1 OES_texture_float and vertex textures support is required."))}let Ea=V.morphAttributes;if((Ea.position!==void 0||Ea.normal!==void 0||Ea.color!==void 0&&ie.isWebGL2===!0)&&Le.update(k,V,Hn),(os||Xe.receiveShadow!==k.receiveShadow)&&(Xe.receiveShadow=k.receiveShadow,Ct.setValue(L,"receiveShadow",k.receiveShadow)),F.isMeshGouraudMaterial&&F.envMap!==null&&(Gn.envMap.value=De,Gn.flipEnvMap.value=De.isCubeTexture&&De.isRenderTargetTexture===!1?-1:1),os&&(Ct.setValue(L,"toneMappingExposure",x.toneMappingExposure),Xe.needsLights&&Bd(Gn,ba),xe&&F.fog===!0&&ee.refreshFogUniforms(Gn,xe),ee.refreshMaterialUniforms(Gn,F,H,q,we),qi.upload(L,Xe.uniformsList,Gn,ye)),F.isShaderMaterial&&F.uniformsNeedUpdate===!0&&(qi.upload(L,Xe.uniformsList,Gn,ye),F.uniformsNeedUpdate=!1),F.isSpriteMaterial&&Ct.setValue(L,"center",k.center),Ct.setValue(L,"modelViewMatrix",k.modelViewMatrix),Ct.setValue(L,"normalMatrix",k.normalMatrix),Ct.setValue(L,"modelMatrix",k.matrixWorld),F.isShaderMaterial||F.isRawShaderMaterial){let Bt=F.uniformsGroups;for(let Ta=0,Vd=Bt.length;Ta0&&ye.useMultisampledRTT(E)===!1?k=me.get(E).__webglMultisampledFramebuffer:Array.isArray(Pe)?k=Pe[V]:k=Pe,T.copy(E.viewport),O.copy(E.scissor),Y=E.scissorTest}else T.copy(K).multiplyScalar(H).floor(),O.copy(D).multiplyScalar(H).floor(),Y=G;if(J.bindFramebuffer(L.FRAMEBUFFER,k)&&ie.drawBuffers&&F&&J.drawBuffers(E,k),J.viewport(T),J.scissor(O),J.setScissorTest(Y),xe){let De=me.get(E.texture);L.framebufferTexture2D(L.FRAMEBUFFER,L.COLOR_ATTACHMENT0,L.TEXTURE_CUBE_MAP_POSITIVE_X+N,De.__webglTexture,V)}else if(Ae){let De=me.get(E.texture),We=N||0;L.framebufferTextureLayer(L.FRAMEBUFFER,L.COLOR_ATTACHMENT0,De.__webglTexture,V||0,We)}I=-1},this.readRenderTargetPixels=function(E,N,V,F,k,xe,Ae){if(!(E&&E.isWebGLRenderTarget)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");return}let Ue=me.get(E).__webglFramebuffer;if(E.isWebGLCubeRenderTarget&&Ae!==void 0&&(Ue=Ue[Ae]),Ue){J.bindFramebuffer(L.FRAMEBUFFER,Ue);try{let De=E.texture,We=De.format,Pe=De.type;if(We!==Wt&&$e.convert(We)!==L.getParameter(L.IMPLEMENTATION_COLOR_READ_FORMAT)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");return}let Ve=Pe===Ts&&(X.has("EXT_color_buffer_half_float")||ie.isWebGL2&&X.has("EXT_color_buffer_float"));if(Pe!==On&&$e.convert(Pe)!==L.getParameter(L.IMPLEMENTATION_COLOR_READ_TYPE)&&!(Pe===xn&&(ie.isWebGL2||X.has("OES_texture_float")||X.has("WEBGL_color_buffer_float")))&&!Ve){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");return}N>=0&&N<=E.width-F&&V>=0&&V<=E.height-k&&L.readPixels(N,V,F,k,$e.convert(We),$e.convert(Pe),xe)}finally{let De=R!==null?me.get(R).__webglFramebuffer:null;J.bindFramebuffer(L.FRAMEBUFFER,De)}}},this.copyFramebufferToTexture=function(E,N,V=0){let F=Math.pow(2,-V),k=Math.floor(N.image.width*F),xe=Math.floor(N.image.height*F);ye.setTexture2D(N,0),L.copyTexSubImage2D(L.TEXTURE_2D,V,0,0,E.x,E.y,k,xe),J.unbindTexture()},this.copyTextureToTexture=function(E,N,V,F=0){let k=N.image.width,xe=N.image.height,Ae=$e.convert(V.format),Ue=$e.convert(V.type);ye.setTexture2D(V,0),L.pixelStorei(L.UNPACK_FLIP_Y_WEBGL,V.flipY),L.pixelStorei(L.UNPACK_PREMULTIPLY_ALPHA_WEBGL,V.premultiplyAlpha),L.pixelStorei(L.UNPACK_ALIGNMENT,V.unpackAlignment),N.isDataTexture?L.texSubImage2D(L.TEXTURE_2D,F,E.x,E.y,k,xe,Ae,Ue,N.image.data):N.isCompressedTexture?L.compressedTexSubImage2D(L.TEXTURE_2D,F,E.x,E.y,N.mipmaps[0].width,N.mipmaps[0].height,Ae,N.mipmaps[0].data):L.texSubImage2D(L.TEXTURE_2D,F,E.x,E.y,Ae,Ue,N.image),F===0&&V.generateMipmaps&&L.generateMipmap(L.TEXTURE_2D),J.unbindTexture()},this.copyTextureToTexture3D=function(E,N,V,F,k=0){if(x.isWebGL1Renderer){console.warn("THREE.WebGLRenderer.copyTextureToTexture3D: can only be used with WebGL2.");return}let xe=E.max.x-E.min.x+1,Ae=E.max.y-E.min.y+1,Ue=E.max.z-E.min.z+1,De=$e.convert(F.format),We=$e.convert(F.type),Pe;if(F.isData3DTexture)ye.setTexture3D(F,0),Pe=L.TEXTURE_3D;else if(F.isDataArrayTexture)ye.setTexture2DArray(F,0),Pe=L.TEXTURE_2D_ARRAY;else{console.warn("THREE.WebGLRenderer.copyTextureToTexture3D: only supports THREE.DataTexture3D and THREE.DataTexture2DArray.");return}L.pixelStorei(L.UNPACK_FLIP_Y_WEBGL,F.flipY),L.pixelStorei(L.UNPACK_PREMULTIPLY_ALPHA_WEBGL,F.premultiplyAlpha),L.pixelStorei(L.UNPACK_ALIGNMENT,F.unpackAlignment);let Ve=L.getParameter(L.UNPACK_ROW_LENGTH),at=L.getParameter(L.UNPACK_IMAGE_HEIGHT),lt=L.getParameter(L.UNPACK_SKIP_PIXELS),Ht=L.getParameter(L.UNPACK_SKIP_ROWS),an=L.getParameter(L.UNPACK_SKIP_IMAGES),ut=V.isCompressedTexture?V.mipmaps[0]:V.image;L.pixelStorei(L.UNPACK_ROW_LENGTH,ut.width),L.pixelStorei(L.UNPACK_IMAGE_HEIGHT,ut.height),L.pixelStorei(L.UNPACK_SKIP_PIXELS,E.min.x),L.pixelStorei(L.UNPACK_SKIP_ROWS,E.min.y),L.pixelStorei(L.UNPACK_SKIP_IMAGES,E.min.z),V.isDataTexture||V.isData3DTexture?L.texSubImage3D(Pe,k,N.x,N.y,N.z,xe,Ae,Ue,De,We,ut.data):V.isCompressedArrayTexture?(console.warn("THREE.WebGLRenderer.copyTextureToTexture3D: untested support for compressed srcTexture."),L.compressedTexSubImage3D(Pe,k,N.x,N.y,N.z,xe,Ae,Ue,De,ut.data)):L.texSubImage3D(Pe,k,N.x,N.y,N.z,xe,Ae,Ue,De,We,ut),L.pixelStorei(L.UNPACK_ROW_LENGTH,Ve),L.pixelStorei(L.UNPACK_IMAGE_HEIGHT,at),L.pixelStorei(L.UNPACK_SKIP_PIXELS,lt),L.pixelStorei(L.UNPACK_SKIP_ROWS,Ht),L.pixelStorei(L.UNPACK_SKIP_IMAGES,an),k===0&&F.generateMipmaps&&L.generateMipmap(Pe),J.unbindTexture()},this.initTexture=function(E){E.isCubeTexture?ye.setTextureCube(E,0):E.isData3DTexture?ye.setTexture3D(E,0):E.isDataArrayTexture||E.isCompressedArrayTexture?ye.setTexture2DArray(E,0):ye.setTexture2D(E,0),J.unbindTexture()},this.resetState=function(){b=0,w=0,R=null,J.reset(),Oe.reset()},typeof __THREE_DEVTOOLS__<"u"&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}get coordinateSystem(){return vn}get outputColorSpace(){return this._outputColorSpace}set outputColorSpace(e){this._outputColorSpace=e;let t=this.getContext();t.drawingBufferColorSpace=e===qc?"display-p3":"srgb",t.unpackColorSpace=Qe.workingColorSpace===va?"display-p3":"srgb"}get physicallyCorrectLights(){return console.warn("THREE.WebGLRenderer: The property .physicallyCorrectLights has been removed. Set renderer.useLegacyLights instead."),!this.useLegacyLights}set physicallyCorrectLights(e){console.warn("THREE.WebGLRenderer: The property .physicallyCorrectLights has been removed. Set renderer.useLegacyLights instead."),this.useLegacyLights=!e}get outputEncoding(){return console.warn("THREE.WebGLRenderer: Property .outputEncoding has been removed. Use .outputColorSpace instead."),this.outputColorSpace===vt?ri:vd}set outputEncoding(e){console.warn("THREE.WebGLRenderer: Property .outputEncoding has been removed. Use .outputColorSpace instead."),this.outputColorSpace=e===ri?vt:Mn}get useLegacyLights(){return console.warn("THREE.WebGLRenderer: The property .useLegacyLights has been deprecated. Migrate your lighting according to the following guide: https://discourse.threejs.org/t/updates-to-lighting-in-three-js-r155/53733."),this._useLegacyLights}set useLegacyLights(e){console.warn("THREE.WebGLRenderer: The property .useLegacyLights has been deprecated. Migrate your lighting according to the following guide: https://discourse.threejs.org/t/updates-to-lighting-in-three-js-r155/53733."),this._useLegacyLights=e}},Co=class extends Ro{};Co.prototype.isWebGL1Renderer=!0;var Po=class s{constructor(e,t=25e-5){this.isFogExp2=!0,this.name="",this.color=new pe(e),this.density=t}clone(){return new s(this.color,this.density)}toJSON(){return{type:"FogExp2",name:this.name,color:this.color.getHex(),density:this.density}}},Lo=class s{constructor(e,t=1,n=1e3){this.isFog=!0,this.name="",this.color=new pe(e),this.near=t,this.far=n}clone(){return new s(this.color,this.near,this.far)}toJSON(){return{type:"Fog",name:this.name,color:this.color.getHex(),near:this.near,far:this.far}}},Io=class extends Je{constructor(){super(),this.isScene=!0,this.type="Scene",this.background=null,this.environment=null,this.fog=null,this.backgroundBlurriness=0,this.backgroundIntensity=1,this.overrideMaterial=null,typeof __THREE_DEVTOOLS__<"u"&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}copy(e,t){return super.copy(e,t),e.background!==null&&(this.background=e.background.clone()),e.environment!==null&&(this.environment=e.environment.clone()),e.fog!==null&&(this.fog=e.fog.clone()),this.backgroundBlurriness=e.backgroundBlurriness,this.backgroundIntensity=e.backgroundIntensity,e.overrideMaterial!==null&&(this.overrideMaterial=e.overrideMaterial.clone()),this.matrixAutoUpdate=e.matrixAutoUpdate,this}toJSON(e){let t=super.toJSON(e);return this.fog!==null&&(t.object.fog=this.fog.toJSON()),this.backgroundBlurriness>0&&(t.object.backgroundBlurriness=this.backgroundBlurriness),this.backgroundIntensity!==1&&(t.object.backgroundIntensity=this.backgroundIntensity),t}},Is=class{constructor(e,t){this.isInterleavedBuffer=!0,this.array=e,this.stride=t,this.count=e!==void 0?e.length/t:0,this.usage=Hr,this.updateRange={offset:0,count:-1},this.version=0,this.uuid=kt()}onUploadCallback(){}set needsUpdate(e){e===!0&&this.version++}setUsage(e){return this.usage=e,this}copy(e){return this.array=new e.array.constructor(e.array),this.count=e.count,this.stride=e.stride,this.usage=e.usage,this}copyAt(e,t,n){e*=this.stride,n*=t.stride;for(let i=0,r=this.stride;ie.far||t.push({distance:c,point:ds.clone(),uv:Un.getInterpolation(ds,hr,ps,ur,Lh,ja,Ih,new Z),face:null,object:this})}copy(e,t){return super.copy(e,t),e.center!==void 0&&this.center.copy(e.center),this.material=e.material,this}};function dr(s,e,t,n,i,r){Ni.subVectors(s,t).addScalar(.5).multiply(n),i!==void 0?(fs.x=r*Ni.x-i*Ni.y,fs.y=i*Ni.x+r*Ni.y):fs.copy(Ni),s.copy(e),s.x+=fs.x,s.y+=fs.y,s.applyMatrix4(Cd)}var fr=new A,Uh=new A,Do=class extends Je{constructor(){super(),this._currentLevel=0,this.type="LOD",Object.defineProperties(this,{levels:{enumerable:!0,value:[]},isLOD:{value:!0}}),this.autoUpdate=!0}copy(e){super.copy(e,!1);let t=e.levels;for(let n=0,i=t.length;n0){let n,i;for(n=1,i=t.length;n0){fr.setFromMatrixPosition(this.matrixWorld);let i=e.ray.origin.distanceTo(fr);this.getObjectForDistance(i).raycast(e,t)}}update(e){let t=this.levels;if(t.length>1){fr.setFromMatrixPosition(e.matrixWorld),Uh.setFromMatrixPosition(this.matrixWorld);let n=fr.distanceTo(Uh)/e.zoom;t[0].object.visible=!0;let i,r;for(i=1,r=t.length;i=a)t[i-1].object.visible=!1,t[i].object.visible=!0;else break}for(this._currentLevel=i-1;ic)continue;d.applyMatrix4(this.matrixWorld);let I=e.ray.origin.distanceTo(d);Ie.far||t.push({distance:I,point:u.clone().applyMatrix4(this.matrixWorld),index:x,face:null,faceIndex:null,object:this})}}else{let p=Math.max(0,a.start),v=Math.min(g.count,a.start+a.count);for(let x=p,y=v-1;xc)continue;d.applyMatrix4(this.matrixWorld);let w=e.ray.origin.distanceTo(d);we.far||t.push({distance:w,point:u.clone().applyMatrix4(this.matrixWorld),index:x,face:null,faceIndex:null,object:this})}}}updateMorphTargets(){let t=this.geometry.morphAttributes,n=Object.keys(t);if(n.length>0){let i=t[n[0]];if(i!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=i.length;r0){let i=t[n[0]];if(i!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=i.length;ri.far)return;r.push({distance:l,distanceToRay:Math.sqrt(o),point:c,index:e,face:null,object:a})}}var Jh=class extends St{constructor(e,t,n,i,r,a,o,c,l){super(e,t,n,i,r,a,o,c,l),this.isVideoTexture=!0,this.minFilter=a!==void 0?a:mt,this.magFilter=r!==void 0?r:mt,this.generateMipmaps=!1;let h=this;function u(){h.needsUpdate=!0,e.requestVideoFrameCallback(u)}"requestVideoFrameCallback"in e&&e.requestVideoFrameCallback(u)}clone(){return new this.constructor(this.image).copy(this)}update(){let e=this.image;"requestVideoFrameCallback"in e===!1&&e.readyState>=e.HAVE_CURRENT_DATA&&(this.needsUpdate=!0)}},$h=class extends St{constructor(e,t){super({width:e,height:t}),this.isFramebufferTexture=!0,this.magFilter=pt,this.minFilter=pt,this.generateMipmaps=!1,this.needsUpdate=!0}},Us=class extends St{constructor(e,t,n,i,r,a,o,c,l,h,u,d){super(null,a,o,c,l,h,i,r,u,d),this.isCompressedTexture=!0,this.image={width:t,height:n},this.mipmaps=e,this.flipY=!1,this.generateMipmaps=!1}},Kh=class extends Us{constructor(e,t,n,i,r,a){super(e,t,n,r,a),this.isCompressedArrayTexture=!0,this.image.depth=i,this.wrapR=It}},Qh=class extends Us{constructor(e,t,n){super(void 0,e[0].width,e[0].height,t,n,zn),this.isCompressedCubeTexture=!0,this.isCubeTexture=!0,this.image=e}},jh=class extends St{constructor(e,t,n,i,r,a,o,c,l){super(e,t,n,i,r,a,o,c,l),this.isCanvasTexture=!0,this.needsUpdate=!0}},Zt=class{constructor(){this.type="Curve",this.arcLengthDivisions=200}getPoint(){return console.warn("THREE.Curve: .getPoint() not implemented."),null}getPointAt(e,t){let n=this.getUtoTmapping(e);return this.getPoint(n,t)}getPoints(e=5){let t=[];for(let n=0;n<=e;n++)t.push(this.getPoint(n/e));return t}getSpacedPoints(e=5){let t=[];for(let n=0;n<=e;n++)t.push(this.getPointAt(n/e));return t}getLength(){let e=this.getLengths();return e[e.length-1]}getLengths(e=this.arcLengthDivisions){if(this.cacheArcLengths&&this.cacheArcLengths.length===e+1&&!this.needsUpdate)return this.cacheArcLengths;this.needsUpdate=!1;let t=[],n,i=this.getPoint(0),r=0;t.push(0);for(let a=1;a<=e;a++)n=this.getPoint(a/e),r+=n.distanceTo(i),t.push(r),i=n;return this.cacheArcLengths=t,t}updateArcLengths(){this.needsUpdate=!0,this.getLengths()}getUtoTmapping(e,t){let n=this.getLengths(),i=0,r=n.length,a;t?a=t:a=e*n[r-1];let o=0,c=r-1,l;for(;o<=c;)if(i=Math.floor(o+(c-o)/2),l=n[i]-a,l<0)o=i+1;else if(l>0)c=i-1;else{c=i;break}if(i=c,n[i]===a)return i/(r-1);let h=n[i],d=n[i+1]-h,f=(a-h)/d;return(i+f)/(r-1)}getTangent(e,t){let i=e-1e-4,r=e+1e-4;i<0&&(i=0),r>1&&(r=1);let a=this.getPoint(i),o=this.getPoint(r),c=t||(a.isVector2?new Z:new A);return c.copy(o).sub(a).normalize(),c}getTangentAt(e,t){let n=this.getUtoTmapping(e);return this.getTangent(n,t)}computeFrenetFrames(e,t){let n=new A,i=[],r=[],a=[],o=new A,c=new ze;for(let f=0;f<=e;f++){let m=f/e;i[f]=this.getTangentAt(m,new A)}r[0]=new A,a[0]=new A;let l=Number.MAX_VALUE,h=Math.abs(i[0].x),u=Math.abs(i[0].y),d=Math.abs(i[0].z);h<=l&&(l=h,n.set(1,0,0)),u<=l&&(l=u,n.set(0,1,0)),d<=l&&n.set(0,0,1),o.crossVectors(i[0],n).normalize(),r[0].crossVectors(i[0],o),a[0].crossVectors(i[0],r[0]);for(let f=1;f<=e;f++){if(r[f]=r[f-1].clone(),a[f]=a[f-1].clone(),o.crossVectors(i[f-1],i[f]),o.length()>Number.EPSILON){o.normalize();let m=Math.acos(ct(i[f-1].dot(i[f]),-1,1));r[f].applyMatrix4(c.makeRotationAxis(o,m))}a[f].crossVectors(i[f],r[f])}if(t===!0){let f=Math.acos(ct(r[0].dot(r[e]),-1,1));f/=e,i[0].dot(o.crossVectors(r[0],r[e]))>0&&(f=-f);for(let m=1;m<=e;m++)r[m].applyMatrix4(c.makeRotationAxis(i[m],f*m)),a[m].crossVectors(i[m],r[m])}return{tangents:i,normals:r,binormals:a}}clone(){return new this.constructor().copy(this)}copy(e){return this.arcLengthDivisions=e.arcLengthDivisions,this}toJSON(){let e={metadata:{version:4.6,type:"Curve",generator:"Curve.toJSON"}};return e.arcLengthDivisions=this.arcLengthDivisions,e.type=this.type,e}fromJSON(e){return this.arcLengthDivisions=e.arcLengthDivisions,this}},Ds=class extends Zt{constructor(e=0,t=0,n=1,i=1,r=0,a=Math.PI*2,o=!1,c=0){super(),this.isEllipseCurve=!0,this.type="EllipseCurve",this.aX=e,this.aY=t,this.xRadius=n,this.yRadius=i,this.aStartAngle=r,this.aEndAngle=a,this.aClockwise=o,this.aRotation=c}getPoint(e,t){let n=t||new Z,i=Math.PI*2,r=this.aEndAngle-this.aStartAngle,a=Math.abs(r)i;)r-=i;r0?0:(Math.floor(Math.abs(o)/r)+1)*r:c===0&&o===r-1&&(o=r-2,c=1);let l,h;this.closed||o>0?l=i[(o-1)%r]:(xr.subVectors(i[0],i[1]).add(i[0]),l=xr);let u=i[o%r],d=i[(o+1)%r];if(this.closed||o+2i.length-2?i.length-1:a+1],u=i[a>i.length-3?i.length-1:a+2];return n.set(eu(o,c.x,l.x,h.x,u.x),eu(o,c.y,l.y,h.y,u.y)),n}copy(e){super.copy(e),this.points=[];for(let t=0,n=e.points.length;t=n){let a=i[r]-n,o=this.curves[r],c=o.getLength(),l=c===0?0:1-a/c;return o.getPointAt(l,t)}r++}return null}getLength(){let e=this.getCurveLengths();return e[e.length-1]}updateArcLengths(){this.needsUpdate=!0,this.cacheLengths=null,this.getCurveLengths()}getCurveLengths(){if(this.cacheLengths&&this.cacheLengths.length===this.curves.length)return this.cacheLengths;let e=[],t=0;for(let n=0,i=this.curves.length;n1&&!t[t.length-1].equals(t[0])&&t.push(t[0]),t}copy(e){super.copy(e),this.curves=[];for(let t=0,n=e.curves.length;t0){let u=l.getPoint(0);u.equals(this.currentPoint)||this.lineTo(u.x,u.y)}this.curves.push(l);let h=l.getPoint(1);return this.currentPoint.copy(h),this}copy(e){return super.copy(e),this.currentPoint.copy(e.currentPoint),this}toJSON(){let e=super.toJSON();return e.currentPoint=this.currentPoint.toArray(),e}fromJSON(e){return super.fromJSON(e),this.currentPoint.fromArray(e.currentPoint),this}},la=class s extends Ge{constructor(e=[new Z(0,-.5),new Z(.5,0),new Z(0,.5)],t=12,n=0,i=Math.PI*2){super(),this.type="LatheGeometry",this.parameters={points:e,segments:t,phiStart:n,phiLength:i},t=Math.floor(t),i=ct(i,0,Math.PI*2);let r=[],a=[],o=[],c=[],l=[],h=1/t,u=new A,d=new Z,f=new A,m=new A,_=new A,g=0,p=0;for(let v=0;v<=e.length-1;v++)switch(v){case 0:g=e[v+1].x-e[v].x,p=e[v+1].y-e[v].y,f.x=p*1,f.y=-g,f.z=p*0,_.copy(f),f.normalize(),c.push(f.x,f.y,f.z);break;case e.length-1:c.push(_.x,_.y,_.z);break;default:g=e[v+1].x-e[v].x,p=e[v+1].y-e[v].y,f.x=p*1,f.y=-g,f.z=p*0,m.copy(f),f.x+=_.x,f.y+=_.y,f.z+=_.z,f.normalize(),c.push(f.x,f.y,f.z),_.copy(m)}for(let v=0;v<=t;v++){let x=n+v*h*i,y=Math.sin(x),b=Math.cos(x);for(let w=0;w<=e.length-1;w++){u.x=e[w].x*y,u.y=e[w].y,u.z=e[w].x*b,a.push(u.x,u.y,u.z),d.x=v/t,d.y=w/(e.length-1),o.push(d.x,d.y);let R=c[3*w+0]*y,I=c[3*w+1],M=c[3*w+0]*b;l.push(R,I,M)}}for(let v=0;v0&&x(!0),t>0&&x(!1)),this.setIndex(h),this.setAttribute("position",new ve(u,3)),this.setAttribute("normal",new ve(d,3)),this.setAttribute("uv",new ve(f,2));function v(){let y=new A,b=new A,w=0,R=(t-e)/n;for(let I=0;I<=r;I++){let M=[],T=I/r,O=T*(t-e)+e;for(let Y=0;Y<=i;Y++){let $=Y/i,U=$*c+o,z=Math.sin(U),q=Math.cos(U);b.x=O*z,b.y=-T*n+g,b.z=O*q,u.push(b.x,b.y,b.z),y.set(z,R,q).normalize(),d.push(y.x,y.y,y.z),f.push($,1-T),M.push(m++)}_.push(M)}for(let I=0;I.9&&R<.1&&(x<.2&&(a[v+0]+=1),y<.2&&(a[v+2]+=1),b<.2&&(a[v+4]+=1))}}function d(v){r.push(v.x,v.y,v.z)}function f(v,x){let y=v*3;x.x=e[y+0],x.y=e[y+1],x.z=e[y+2]}function m(){let v=new A,x=new A,y=new A,b=new A,w=new Z,R=new Z,I=new Z;for(let M=0,T=0;M80*t){o=l=s[0],c=h=s[1];for(let m=t;ml&&(l=u),d>h&&(h=d);f=Math.max(l-o,h-c),f=f!==0?32767/f:0}return Os(r,a,t,o,c,f,0),a}};function Pd(s,e,t,n,i){let r,a;if(i===gx(s,e,t,n)>0)for(r=e;r=e;r-=n)a=tu(r,s[r],s[r+1],a);return a&&Ma(a,a.next)&&(Bs(a),a=a.next),a}function fi(s,e){if(!s)return s;e||(e=s);let t=s,n;do if(n=!1,!t.steiner&&(Ma(t,t.next)||st(t.prev,t,t.next)===0)){if(Bs(t),t=e=t.prev,t===t.next)break;n=!0}else t=t.next;while(n||t!==e);return e}function Os(s,e,t,n,i,r,a){if(!s)return;!a&&r&&hx(s,n,i,r);let o=s,c,l;for(;s.prev!==s.next;){if(c=s.prev,l=s.next,r?nx(s,n,i,r):tx(s)){e.push(c.i/t|0),e.push(s.i/t|0),e.push(l.i/t|0),Bs(s),s=l.next,o=l.next;continue}if(s=l,s===o){a?a===1?(s=ix(fi(s),e,t),Os(s,e,t,n,i,r,2)):a===2&&sx(s,e,t,n,i,r):Os(fi(s),e,t,n,i,r,1);break}}}function tx(s){let e=s.prev,t=s,n=s.next;if(st(e,t,n)>=0)return!1;let i=e.x,r=t.x,a=n.x,o=e.y,c=t.y,l=n.y,h=ir?i>a?i:a:r>a?r:a,f=o>c?o>l?o:l:c>l?c:l,m=n.next;for(;m!==e;){if(m.x>=h&&m.x<=d&&m.y>=u&&m.y<=f&&Gi(i,o,r,c,a,l,m.x,m.y)&&st(m.prev,m,m.next)>=0)return!1;m=m.next}return!0}function nx(s,e,t,n){let i=s.prev,r=s,a=s.next;if(st(i,r,a)>=0)return!1;let o=i.x,c=r.x,l=a.x,h=i.y,u=r.y,d=a.y,f=oc?o>l?o:l:c>l?c:l,g=h>u?h>d?h:d:u>d?u:d,p=Ko(f,m,e,t,n),v=Ko(_,g,e,t,n),x=s.prevZ,y=s.nextZ;for(;x&&x.z>=p&&y&&y.z<=v;){if(x.x>=f&&x.x<=_&&x.y>=m&&x.y<=g&&x!==i&&x!==a&&Gi(o,h,c,u,l,d,x.x,x.y)&&st(x.prev,x,x.next)>=0||(x=x.prevZ,y.x>=f&&y.x<=_&&y.y>=m&&y.y<=g&&y!==i&&y!==a&&Gi(o,h,c,u,l,d,y.x,y.y)&&st(y.prev,y,y.next)>=0))return!1;y=y.nextZ}for(;x&&x.z>=p;){if(x.x>=f&&x.x<=_&&x.y>=m&&x.y<=g&&x!==i&&x!==a&&Gi(o,h,c,u,l,d,x.x,x.y)&&st(x.prev,x,x.next)>=0)return!1;x=x.prevZ}for(;y&&y.z<=v;){if(y.x>=f&&y.x<=_&&y.y>=m&&y.y<=g&&y!==i&&y!==a&&Gi(o,h,c,u,l,d,y.x,y.y)&&st(y.prev,y,y.next)>=0)return!1;y=y.nextZ}return!0}function ix(s,e,t){let n=s;do{let i=n.prev,r=n.next.next;!Ma(i,r)&&Ld(i,n,n.next,r)&&Fs(i,r)&&Fs(r,i)&&(e.push(i.i/t|0),e.push(n.i/t|0),e.push(r.i/t|0),Bs(n),Bs(n.next),n=s=r),n=n.next}while(n!==s);return fi(n)}function sx(s,e,t,n,i,r){let a=s;do{let o=a.next.next;for(;o!==a.prev;){if(a.i!==o.i&&fx(a,o)){let c=Id(a,o);a=fi(a,a.next),c=fi(c,c.next),Os(a,e,t,n,i,r,0),Os(c,e,t,n,i,r,0);return}o=o.next}a=a.next}while(a!==s)}function rx(s,e,t,n){let i=[],r,a,o,c,l;for(r=0,a=e.length;r=t.next.y&&t.next.y!==t.y){let d=t.x+(a-t.y)*(t.next.x-t.x)/(t.next.y-t.y);if(d<=r&&d>n&&(n=d,i=t.x=t.x&&t.x>=c&&r!==t.x&&Gi(ai.x||t.x===i.x&&lx(i,t)))&&(i=t,h=u)),t=t.next;while(t!==o);return i}function lx(s,e){return st(s.prev,s,e.prev)<0&&st(e.next,s,s.next)<0}function hx(s,e,t,n){let i=s;do i.z===0&&(i.z=Ko(i.x,i.y,e,t,n)),i.prevZ=i.prev,i.nextZ=i.next,i=i.next;while(i!==s);i.prevZ.nextZ=null,i.prevZ=null,ux(i)}function ux(s){let e,t,n,i,r,a,o,c,l=1;do{for(t=s,s=null,r=null,a=0;t;){for(a++,n=t,o=0,e=0;e0||c>0&&n;)o!==0&&(c===0||!n||t.z<=n.z)?(i=t,t=t.nextZ,o--):(i=n,n=n.nextZ,c--),r?r.nextZ=i:s=i,i.prevZ=r,r=i;t=n}r.nextZ=null,l*=2}while(a>1);return s}function Ko(s,e,t,n,i){return s=(s-t)*i|0,e=(e-n)*i|0,s=(s|s<<8)&16711935,s=(s|s<<4)&252645135,s=(s|s<<2)&858993459,s=(s|s<<1)&1431655765,e=(e|e<<8)&16711935,e=(e|e<<4)&252645135,e=(e|e<<2)&858993459,e=(e|e<<1)&1431655765,s|e<<1}function dx(s){let e=s,t=s;do(e.x=(s-a)*(r-o)&&(s-a)*(n-o)>=(t-a)*(e-o)&&(t-a)*(r-o)>=(i-a)*(n-o)}function fx(s,e){return s.next.i!==e.i&&s.prev.i!==e.i&&!px(s,e)&&(Fs(s,e)&&Fs(e,s)&&mx(s,e)&&(st(s.prev,s,e.prev)||st(s,e.prev,e))||Ma(s,e)&&st(s.prev,s,s.next)>0&&st(e.prev,e,e.next)>0)}function st(s,e,t){return(e.y-s.y)*(t.x-e.x)-(e.x-s.x)*(t.y-e.y)}function Ma(s,e){return s.x===e.x&&s.y===e.y}function Ld(s,e,t,n){let i=br(st(s,e,t)),r=br(st(s,e,n)),a=br(st(t,n,s)),o=br(st(t,n,e));return!!(i!==r&&a!==o||i===0&&Sr(s,t,e)||r===0&&Sr(s,n,e)||a===0&&Sr(t,s,n)||o===0&&Sr(t,e,n))}function Sr(s,e,t){return e.x<=Math.max(s.x,t.x)&&e.x>=Math.min(s.x,t.x)&&e.y<=Math.max(s.y,t.y)&&e.y>=Math.min(s.y,t.y)}function br(s){return s>0?1:s<0?-1:0}function px(s,e){let t=s;do{if(t.i!==s.i&&t.next.i!==s.i&&t.i!==e.i&&t.next.i!==e.i&&Ld(t,t.next,s,e))return!0;t=t.next}while(t!==s);return!1}function Fs(s,e){return st(s.prev,s,s.next)<0?st(s,e,s.next)>=0&&st(s,s.prev,e)>=0:st(s,e,s.prev)<0||st(s,s.next,e)<0}function mx(s,e){let t=s,n=!1,i=(s.x+e.x)/2,r=(s.y+e.y)/2;do t.y>r!=t.next.y>r&&t.next.y!==t.y&&i<(t.next.x-t.x)*(r-t.y)/(t.next.y-t.y)+t.x&&(n=!n),t=t.next;while(t!==s);return n}function Id(s,e){let t=new Qo(s.i,s.x,s.y),n=new Qo(e.i,e.x,e.y),i=s.next,r=e.prev;return s.next=e,e.prev=s,t.next=i,i.prev=t,n.next=t,t.prev=n,r.next=n,n.prev=r,n}function tu(s,e,t,n){let i=new Qo(s,e,t);return n?(i.next=n.next,i.prev=n,n.next.prev=i,n.next=i):(i.prev=i,i.next=i),i}function Bs(s){s.next.prev=s.prev,s.prev.next=s.next,s.prevZ&&(s.prevZ.nextZ=s.nextZ),s.nextZ&&(s.nextZ.prevZ=s.prevZ)}function Qo(s,e,t){this.i=s,this.x=e,this.y=t,this.prev=null,this.next=null,this.z=0,this.prevZ=null,this.nextZ=null,this.steiner=!1}function gx(s,e,t,n){let i=0;for(let r=e,a=t-n;r2&&s[e-1].equals(s[0])&&s.pop()}function iu(s,e){for(let t=0;tNumber.EPSILON){let S=Math.sqrt(rt),B=Math.sqrt(Ne*Ne+qe*qe),ee=oe.x-ye/S,j=oe.y+me/S,te=X.x-qe/B,Me=X.y+Ne/B,re=((te-ee)*qe-(Me-j)*Ne)/(me*qe-ye*Ne);ie=ee+me*re-L.x,J=j+ye*re-L.y;let de=ie*ie+J*J;if(de<=2)return new Z(ie,J);Se=Math.sqrt(de/2)}else{let S=!1;me>Number.EPSILON?Ne>Number.EPSILON&&(S=!0):me<-Number.EPSILON?Ne<-Number.EPSILON&&(S=!0):Math.sign(ye)===Math.sign(qe)&&(S=!0),S?(ie=-ye,J=me,Se=Math.sqrt(rt)):(ie=me,J=ye,Se=Math.sqrt(rt/2))}return new Z(ie/Se,J/Se)}let W=[];for(let L=0,oe=U.length,X=oe-1,ie=L+1;L=0;L--){let oe=L/g,X=f*Math.cos(oe*Math.PI/2),ie=m*Math.sin(oe*Math.PI/2)+_;for(let J=0,Se=U.length;J=0;){let ie=X,J=X-1;J<0&&(J=L.length-1);for(let Se=0,me=h+g*2;Se0)&&f.push(x,y,w),(p!==n-1||c0!=e>0&&this.version++,this._anisotropy=e}get clearcoat(){return this._clearcoat}set clearcoat(e){this._clearcoat>0!=e>0&&this.version++,this._clearcoat=e}get iridescence(){return this._iridescence}set iridescence(e){this._iridescence>0!=e>0&&this.version++,this._iridescence=e}get sheen(){return this._sheen}set sheen(e){this._sheen>0!=e>0&&this.version++,this._sheen=e}get transmission(){return this._transmission}set transmission(e){this._transmission>0!=e>0&&this.version++,this._transmission=e}copy(e){return super.copy(e),this.defines={STANDARD:"",PHYSICAL:""},this.anisotropy=e.anisotropy,this.anisotropyRotation=e.anisotropyRotation,this.anisotropyMap=e.anisotropyMap,this.clearcoat=e.clearcoat,this.clearcoatMap=e.clearcoatMap,this.clearcoatRoughness=e.clearcoatRoughness,this.clearcoatRoughnessMap=e.clearcoatRoughnessMap,this.clearcoatNormalMap=e.clearcoatNormalMap,this.clearcoatNormalScale.copy(e.clearcoatNormalScale),this.ior=e.ior,this.iridescence=e.iridescence,this.iridescenceMap=e.iridescenceMap,this.iridescenceIOR=e.iridescenceIOR,this.iridescenceThicknessRange=[...e.iridescenceThicknessRange],this.iridescenceThicknessMap=e.iridescenceThicknessMap,this.sheen=e.sheen,this.sheenColor.copy(e.sheenColor),this.sheenColorMap=e.sheenColorMap,this.sheenRoughness=e.sheenRoughness,this.sheenRoughnessMap=e.sheenRoughnessMap,this.transmission=e.transmission,this.transmissionMap=e.transmissionMap,this.thickness=e.thickness,this.thicknessMap=e.thicknessMap,this.attenuationDistance=e.attenuationDistance,this.attenuationColor.copy(e.attenuationColor),this.specularIntensity=e.specularIntensity,this.specularIntensityMap=e.specularIntensityMap,this.specularColor.copy(e.specularColor),this.specularColorMap=e.specularColorMap,this}},uc=class extends bt{constructor(e){super(),this.isMeshPhongMaterial=!0,this.type="MeshPhongMaterial",this.color=new pe(16777215),this.specular=new pe(1118481),this.shininess=30,this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new pe(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=mi,this.normalScale=new Z(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.combine=xa,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.flatShading=!1,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.specular.copy(e.specular),this.shininess=e.shininess,this.map=e.map,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.emissive.copy(e.emissive),this.emissiveMap=e.emissiveMap,this.emissiveIntensity=e.emissiveIntensity,this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.specularMap=e.specularMap,this.alphaMap=e.alphaMap,this.envMap=e.envMap,this.combine=e.combine,this.reflectivity=e.reflectivity,this.refractionRatio=e.refractionRatio,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.flatShading=e.flatShading,this.fog=e.fog,this}},dc=class extends bt{constructor(e){super(),this.isMeshToonMaterial=!0,this.defines={TOON:""},this.type="MeshToonMaterial",this.color=new pe(16777215),this.map=null,this.gradientMap=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new pe(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=mi,this.normalScale=new Z(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.alphaMap=null,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.map=e.map,this.gradientMap=e.gradientMap,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.emissive.copy(e.emissive),this.emissiveMap=e.emissiveMap,this.emissiveIntensity=e.emissiveIntensity,this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.alphaMap=e.alphaMap,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.fog=e.fog,this}},fc=class extends bt{constructor(e){super(),this.isMeshNormalMaterial=!0,this.type="MeshNormalMaterial",this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=mi,this.normalScale=new Z(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.flatShading=!1,this.setValues(e)}copy(e){return super.copy(e),this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.flatShading=e.flatShading,this}},pc=class extends bt{constructor(e){super(),this.isMeshLambertMaterial=!0,this.type="MeshLambertMaterial",this.color=new pe(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new pe(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=mi,this.normalScale=new Z(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.combine=xa,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.flatShading=!1,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.color.copy(e.color),this.map=e.map,this.lightMap=e.lightMap,this.lightMapIntensity=e.lightMapIntensity,this.aoMap=e.aoMap,this.aoMapIntensity=e.aoMapIntensity,this.emissive.copy(e.emissive),this.emissiveMap=e.emissiveMap,this.emissiveIntensity=e.emissiveIntensity,this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.specularMap=e.specularMap,this.alphaMap=e.alphaMap,this.envMap=e.envMap,this.combine=e.combine,this.reflectivity=e.reflectivity,this.refractionRatio=e.refractionRatio,this.wireframe=e.wireframe,this.wireframeLinewidth=e.wireframeLinewidth,this.wireframeLinecap=e.wireframeLinecap,this.wireframeLinejoin=e.wireframeLinejoin,this.flatShading=e.flatShading,this.fog=e.fog,this}},mc=class extends bt{constructor(e){super(),this.isMeshMatcapMaterial=!0,this.defines={MATCAP:""},this.type="MeshMatcapMaterial",this.color=new pe(16777215),this.matcap=null,this.map=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=mi,this.normalScale=new Z(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.alphaMap=null,this.flatShading=!1,this.fog=!0,this.setValues(e)}copy(e){return super.copy(e),this.defines={MATCAP:""},this.color.copy(e.color),this.matcap=e.matcap,this.map=e.map,this.bumpMap=e.bumpMap,this.bumpScale=e.bumpScale,this.normalMap=e.normalMap,this.normalMapType=e.normalMapType,this.normalScale.copy(e.normalScale),this.displacementMap=e.displacementMap,this.displacementScale=e.displacementScale,this.displacementBias=e.displacementBias,this.alphaMap=e.alphaMap,this.flatShading=e.flatShading,this.fog=e.fog,this}},gc=class extends wt{constructor(e){super(),this.isLineDashedMaterial=!0,this.type="LineDashedMaterial",this.scale=1,this.dashSize=3,this.gapSize=1,this.setValues(e)}copy(e){return super.copy(e),this.scale=e.scale,this.dashSize=e.dashSize,this.gapSize=e.gapSize,this}};function ni(s,e,t){return!s||!t&&s.constructor===e?s:typeof e.BYTES_PER_ELEMENT=="number"?new e(s):Array.prototype.slice.call(s)}function Ud(s){return ArrayBuffer.isView(s)&&!(s instanceof DataView)}function Dd(s){function e(i,r){return s[i]-s[r]}let t=s.length,n=new Array(t);for(let i=0;i!==t;++i)n[i]=i;return n.sort(e),n}function _c(s,e,t){let n=s.length,i=new s.constructor(n);for(let r=0,a=0;a!==n;++r){let o=t[r]*e;for(let c=0;c!==e;++c)i[a++]=s[o+c]}return i}function $c(s,e,t,n){let i=1,r=s[0];for(;r!==void 0&&r[n]===void 0;)r=s[i++];if(r===void 0)return;let a=r[n];if(a!==void 0)if(Array.isArray(a))do a=r[n],a!==void 0&&(e.push(r.time),t.push.apply(t,a)),r=s[i++];while(r!==void 0);else if(a.toArray!==void 0)do a=r[n],a!==void 0&&(e.push(r.time),a.toArray(t,t.length)),r=s[i++];while(r!==void 0);else do a=r[n],a!==void 0&&(e.push(r.time),t.push(a)),r=s[i++];while(r!==void 0)}function yx(s,e,t,n,i=30){let r=s.clone();r.name=e;let a=[];for(let c=0;c=n)){u.push(l.times[f]);for(let _=0;_r.tracks[c].times[0]&&(o=r.tracks[c].times[0]);for(let c=0;c=o.times[m]){let p=m*u+h,v=p+u-h;_=o.values.slice(p,v)}else{let p=o.createInterpolant(),v=h,x=u-h;p.evaluate(r),_=p.resultBuffer.slice(v,x)}c==="quaternion"&&new Ut().fromArray(_).normalize().conjugate().toArray(_);let g=l.times.length;for(let p=0;p=r)){let o=t[1];e=r)break t}a=n,n=0;break n}break e}for(;n>>1;et;)--a;if(++a,r!==0||a!==i){r>=a&&(a=Math.max(a,1),r=a-1);let o=this.getValueSize();this.times=n.slice(r,a),this.values=this.values.slice(r*o,a*o)}return this}validate(){let e=!0,t=this.getValueSize();t-Math.floor(t)!==0&&(console.error("THREE.KeyframeTrack: Invalid value size in track.",this),e=!1);let n=this.times,i=this.values,r=n.length;r===0&&(console.error("THREE.KeyframeTrack: Track is empty.",this),e=!1);let a=null;for(let o=0;o!==r;o++){let c=n[o];if(typeof c=="number"&&isNaN(c)){console.error("THREE.KeyframeTrack: Time is not a valid number.",this,o,c),e=!1;break}if(a!==null&&a>c){console.error("THREE.KeyframeTrack: Out of order keys.",this,o,c,a),e=!1;break}a=c}if(i!==void 0&&Ud(i))for(let o=0,c=i.length;o!==c;++o){let l=i[o];if(isNaN(l)){console.error("THREE.KeyframeTrack: Value is not a valid number.",this,o,l),e=!1;break}}return e}optimize(){let e=this.times.slice(),t=this.values.slice(),n=this.getValueSize(),i=this.getInterpolation()===La,r=e.length-1,a=1;for(let o=1;o0){e[a]=e[r];for(let o=r*n,c=a*n,l=0;l!==n;++l)t[c+l]=t[o+l];++a}return a!==e.length?(this.times=e.slice(0,a),this.values=t.slice(0,a*n)):(this.times=e,this.values=t),this}clone(){let e=this.times.slice(),t=this.values.slice(),n=this.constructor,i=new n(this.name,e,t);return i.createInterpolant=this.createInterpolant,i}};Jt.prototype.TimeBufferType=Float32Array;Jt.prototype.ValueBufferType=Float32Array;Jt.prototype.DefaultInterpolation=Fr;var Vn=class extends Jt{};Vn.prototype.ValueTypeName="bool";Vn.prototype.ValueBufferType=Array;Vn.prototype.DefaultInterpolation=Or;Vn.prototype.InterpolantFactoryMethodLinear=void 0;Vn.prototype.InterpolantFactoryMethodSmooth=void 0;var pa=class extends Jt{};pa.prototype.ValueTypeName="color";var ts=class extends Jt{};ts.prototype.ValueTypeName="number";var yc=class extends es{constructor(e,t,n,i){super(e,t,n,i)}interpolate_(e,t,n,i){let r=this.resultBuffer,a=this.sampleValues,o=this.valueSize,c=(n-t)/(i-t),l=e*o;for(let h=l+o;l!==h;l+=4)Ut.slerpFlat(r,0,a,l-o,a,l,c);return r}},pi=class extends Jt{InterpolantFactoryMethodLinear(e){return new yc(this.times,this.values,this.getValueSize(),e)}};pi.prototype.ValueTypeName="quaternion";pi.prototype.DefaultInterpolation=Fr;pi.prototype.InterpolantFactoryMethodSmooth=void 0;var kn=class extends Jt{};kn.prototype.ValueTypeName="string";kn.prototype.ValueBufferType=Array;kn.prototype.DefaultInterpolation=Or;kn.prototype.InterpolantFactoryMethodLinear=void 0;kn.prototype.InterpolantFactoryMethodSmooth=void 0;var ns=class extends Jt{};ns.prototype.ValueTypeName="vector";var is=class{constructor(e,t=-1,n,i=Xc){this.name=e,this.tracks=n,this.duration=t,this.blendMode=i,this.uuid=kt(),this.duration<0&&this.resetDuration()}static parse(e){let t=[],n=e.tracks,i=1/(e.fps||1);for(let a=0,o=n.length;a!==o;++a)t.push(bx(n[a]).scale(i));let r=new this(e.name,e.duration,t,e.blendMode);return r.uuid=e.uuid,r}static toJSON(e){let t=[],n=e.tracks,i={name:e.name,duration:e.duration,tracks:t,uuid:e.uuid,blendMode:e.blendMode};for(let r=0,a=n.length;r!==a;++r)t.push(Jt.toJSON(n[r]));return i}static CreateFromMorphTargetSequence(e,t,n,i){let r=t.length,a=[];for(let o=0;o1){let u=h[1],d=i[u];d||(i[u]=d=[]),d.push(l)}}let a=[];for(let o in i)a.push(this.CreateFromMorphTargetSequence(o,i[o],t,n));return a}static parseAnimation(e,t){if(!e)return console.error("THREE.AnimationClip: No animation in JSONLoader data."),null;let n=function(u,d,f,m,_){if(f.length!==0){let g=[],p=[];$c(f,g,p,m),g.length!==0&&_.push(new u(d,g,p))}},i=[],r=e.name||"default",a=e.fps||30,o=e.blendMode,c=e.length||-1,l=e.hierarchy||[];for(let u=0;u{t&&t(r),this.manager.itemEnd(e)},0),r;if(fn[e]!==void 0){fn[e].push({onLoad:t,onProgress:n,onError:i});return}fn[e]=[],fn[e].push({onLoad:t,onProgress:n,onError:i});let a=new Request(e,{headers:new Headers(this.requestHeader),credentials:this.withCredentials?"include":"same-origin"}),o=this.mimeType,c=this.responseType;fetch(a).then(l=>{if(l.status===200||l.status===0){if(l.status===0&&console.warn("THREE.FileLoader: HTTP Status 0 received."),typeof ReadableStream>"u"||l.body===void 0||l.body.getReader===void 0)return l;let h=fn[e],u=l.body.getReader(),d=l.headers.get("Content-Length")||l.headers.get("X-File-Size"),f=d?parseInt(d):0,m=f!==0,_=0,g=new ReadableStream({start(p){v();function v(){u.read().then(({done:x,value:y})=>{if(x)p.close();else{_+=y.byteLength;let b=new ProgressEvent("progress",{lengthComputable:m,loaded:_,total:f});for(let w=0,R=h.length;w{switch(c){case"arraybuffer":return l.arrayBuffer();case"blob":return l.blob();case"document":return l.text().then(h=>new DOMParser().parseFromString(h,o));case"json":return l.json();default:if(o===void 0)return l.text();{let u=/charset="?([^;"\s]*)"?/i.exec(o),d=u&&u[1]?u[1].toLowerCase():void 0,f=new TextDecoder(d);return l.arrayBuffer().then(m=>f.decode(m))}}}).then(l=>{ss.add(e,l);let h=fn[e];delete fn[e];for(let u=0,d=h.length;u{let h=fn[e];if(h===void 0)throw this.manager.itemError(e),l;delete fn[e];for(let u=0,d=h.length;u{this.manager.itemEnd(e)}),this.manager.itemStart(e)}setResponseType(e){return this.responseType=e,this}setMimeType(e){return this.mimeType=e,this}},au=class extends Dt{constructor(e){super(e)}load(e,t,n,i){let r=this,a=new rn(this.manager);a.setPath(this.path),a.setRequestHeader(this.requestHeader),a.setWithCredentials(this.withCredentials),a.load(e,function(o){try{t(r.parse(JSON.parse(o)))}catch(c){i?i(c):console.error(c),r.manager.itemError(e)}},n,i)}parse(e){let t=[];for(let n=0;n0:i.vertexColors=e.vertexColors),e.uniforms!==void 0)for(let r in e.uniforms){let a=e.uniforms[r];switch(i.uniforms[r]={},a.type){case"t":i.uniforms[r].value=n(a.value);break;case"c":i.uniforms[r].value=new pe().setHex(a.value);break;case"v2":i.uniforms[r].value=new Z().fromArray(a.value);break;case"v3":i.uniforms[r].value=new A().fromArray(a.value);break;case"v4":i.uniforms[r].value=new je().fromArray(a.value);break;case"m3":i.uniforms[r].value=new He().fromArray(a.value);break;case"m4":i.uniforms[r].value=new ze().fromArray(a.value);break;default:i.uniforms[r].value=a.value}}if(e.defines!==void 0&&(i.defines=e.defines),e.vertexShader!==void 0&&(i.vertexShader=e.vertexShader),e.fragmentShader!==void 0&&(i.fragmentShader=e.fragmentShader),e.glslVersion!==void 0&&(i.glslVersion=e.glslVersion),e.extensions!==void 0)for(let r in e.extensions)i.extensions[r]=e.extensions[r];if(e.lights!==void 0&&(i.lights=e.lights),e.clipping!==void 0&&(i.clipping=e.clipping),e.size!==void 0&&(i.size=e.size),e.sizeAttenuation!==void 0&&(i.sizeAttenuation=e.sizeAttenuation),e.map!==void 0&&(i.map=n(e.map)),e.matcap!==void 0&&(i.matcap=n(e.matcap)),e.alphaMap!==void 0&&(i.alphaMap=n(e.alphaMap)),e.bumpMap!==void 0&&(i.bumpMap=n(e.bumpMap)),e.bumpScale!==void 0&&(i.bumpScale=e.bumpScale),e.normalMap!==void 0&&(i.normalMap=n(e.normalMap)),e.normalMapType!==void 0&&(i.normalMapType=e.normalMapType),e.normalScale!==void 0){let r=e.normalScale;Array.isArray(r)===!1&&(r=[r,r]),i.normalScale=new Z().fromArray(r)}return e.displacementMap!==void 0&&(i.displacementMap=n(e.displacementMap)),e.displacementScale!==void 0&&(i.displacementScale=e.displacementScale),e.displacementBias!==void 0&&(i.displacementBias=e.displacementBias),e.roughnessMap!==void 0&&(i.roughnessMap=n(e.roughnessMap)),e.metalnessMap!==void 0&&(i.metalnessMap=n(e.metalnessMap)),e.emissiveMap!==void 0&&(i.emissiveMap=n(e.emissiveMap)),e.emissiveIntensity!==void 0&&(i.emissiveIntensity=e.emissiveIntensity),e.specularMap!==void 0&&(i.specularMap=n(e.specularMap)),e.specularIntensityMap!==void 0&&(i.specularIntensityMap=n(e.specularIntensityMap)),e.specularColorMap!==void 0&&(i.specularColorMap=n(e.specularColorMap)),e.envMap!==void 0&&(i.envMap=n(e.envMap)),e.envMapIntensity!==void 0&&(i.envMapIntensity=e.envMapIntensity),e.reflectivity!==void 0&&(i.reflectivity=e.reflectivity),e.refractionRatio!==void 0&&(i.refractionRatio=e.refractionRatio),e.lightMap!==void 0&&(i.lightMap=n(e.lightMap)),e.lightMapIntensity!==void 0&&(i.lightMapIntensity=e.lightMapIntensity),e.aoMap!==void 0&&(i.aoMap=n(e.aoMap)),e.aoMapIntensity!==void 0&&(i.aoMapIntensity=e.aoMapIntensity),e.gradientMap!==void 0&&(i.gradientMap=n(e.gradientMap)),e.clearcoatMap!==void 0&&(i.clearcoatMap=n(e.clearcoatMap)),e.clearcoatRoughnessMap!==void 0&&(i.clearcoatRoughnessMap=n(e.clearcoatRoughnessMap)),e.clearcoatNormalMap!==void 0&&(i.clearcoatNormalMap=n(e.clearcoatNormalMap)),e.clearcoatNormalScale!==void 0&&(i.clearcoatNormalScale=new Z().fromArray(e.clearcoatNormalScale)),e.iridescenceMap!==void 0&&(i.iridescenceMap=n(e.iridescenceMap)),e.iridescenceThicknessMap!==void 0&&(i.iridescenceThicknessMap=n(e.iridescenceThicknessMap)),e.transmissionMap!==void 0&&(i.transmissionMap=n(e.transmissionMap)),e.thicknessMap!==void 0&&(i.thicknessMap=n(e.thicknessMap)),e.anisotropyMap!==void 0&&(i.anisotropyMap=n(e.anisotropyMap)),e.sheenColorMap!==void 0&&(i.sheenColorMap=n(e.sheenColorMap)),e.sheenRoughnessMap!==void 0&&(i.sheenRoughnessMap=n(e.sheenRoughnessMap)),i}setTextures(e){return this.textures=e,this}static createMaterialFromType(e){let t={ShadowMaterial:cc,SpriteMaterial:ea,RawShaderMaterial:lc,ShaderMaterial:jt,PointsMaterial:na,MeshPhysicalMaterial:hc,MeshStandardMaterial:da,MeshPhongMaterial:uc,MeshToonMaterial:dc,MeshNormalMaterial:fc,MeshLambertMaterial:pc,MeshDepthMaterial:Qr,MeshDistanceMaterial:jr,MeshBasicMaterial:Sn,MeshMatcapMaterial:mc,LineDashedMaterial:gc,LineBasicMaterial:wt,Material:bt};return new t[e]}},ga=class{static decodeText(e){if(typeof TextDecoder<"u")return new TextDecoder().decode(e);let t="";for(let n=0,i=e.length;n0){let c=new ma(t);r=new rs(c),r.setCrossOrigin(this.crossOrigin);for(let l=0,h=e.length;l0){i=new rs(this.manager),i.setCrossOrigin(this.crossOrigin);for(let a=0,o=e.length;a"u"&&console.warn("THREE.ImageBitmapLoader: createImageBitmap() not supported."),typeof fetch>"u"&&console.warn("THREE.ImageBitmapLoader: fetch() not supported."),this.options={premultiplyAlpha:"none"}}setOptions(e){return this.options=e,this}load(e,t,n,i){e===void 0&&(e=""),this.path!==void 0&&(e=this.path+e),e=this.manager.resolveURL(e);let r=this,a=ss.get(e);if(a!==void 0)return r.manager.itemStart(e),setTimeout(function(){t&&t(a),r.manager.itemEnd(e)},0),a;let o={};o.credentials=this.crossOrigin==="anonymous"?"same-origin":"include",o.headers=this.requestHeader,fetch(e,o).then(function(c){return c.blob()}).then(function(c){return createImageBitmap(c,Object.assign(r.options,{colorSpaceConversion:"none"}))}).then(function(c){ss.add(e,c),t&&t(c),r.manager.itemEnd(e)}).catch(function(c){i&&i(c),r.manager.itemError(e),r.manager.itemEnd(e)}),r.manager.itemStart(e)}},Er,_a=class{static getContext(){return Er===void 0&&(Er=new(window.AudioContext||window.webkitAudioContext)),Er}static setContext(e){Er=e}},xu=class extends Dt{constructor(e){super(e)}load(e,t,n,i){let r=this,a=new rn(this.manager);a.setResponseType("arraybuffer"),a.setPath(this.path),a.setRequestHeader(this.requestHeader),a.setWithCredentials(this.withCredentials),a.load(e,function(c){try{let l=c.slice(0);_a.getContext().decodeAudioData(l,function(u){t(u)},o)}catch(l){o(l)}},n,i);function o(c){i?i(c):console.error(c),r.manager.itemError(e)}}},vu=new ze,yu=new ze,Zn=new ze,Mu=class{constructor(){this.type="StereoCamera",this.aspect=1,this.eyeSep=.064,this.cameraL=new yt,this.cameraL.layers.enable(1),this.cameraL.matrixAutoUpdate=!1,this.cameraR=new yt,this.cameraR.layers.enable(2),this.cameraR.matrixAutoUpdate=!1,this._cache={focus:null,fov:null,aspect:null,near:null,far:null,zoom:null,eyeSep:null}}update(e){let t=this._cache;if(t.focus!==e.focus||t.fov!==e.fov||t.aspect!==e.aspect*this.aspect||t.near!==e.near||t.far!==e.far||t.zoom!==e.zoom||t.eyeSep!==this.eyeSep){t.focus=e.focus,t.fov=e.fov,t.aspect=e.aspect*this.aspect,t.near=e.near,t.far=e.far,t.zoom=e.zoom,t.eyeSep=this.eyeSep,Zn.copy(e.projectionMatrix);let i=t.eyeSep/2,r=i*t.near/t.focus,a=t.near*Math.tan(ai*t.fov*.5)/t.zoom,o,c;yu.elements[12]=-i,vu.elements[12]=i,o=-a*t.aspect+r,c=a*t.aspect+r,Zn.elements[0]=2*t.near/(c-o),Zn.elements[8]=(c+o)/(c-o),this.cameraL.projectionMatrix.copy(Zn),o=-a*t.aspect-r,c=a*t.aspect-r,Zn.elements[0]=2*t.near/(c-o),Zn.elements[8]=(c+o)/(c-o),this.cameraR.projectionMatrix.copy(Zn)}this.cameraL.matrixWorld.copy(e.matrixWorld).multiply(yu),this.cameraR.matrixWorld.copy(e.matrixWorld).multiply(vu)}},Oc=class{constructor(e=!0){this.autoStart=e,this.startTime=0,this.oldTime=0,this.elapsedTime=0,this.running=!1}start(){this.startTime=Su(),this.oldTime=this.startTime,this.elapsedTime=0,this.running=!0}stop(){this.getElapsedTime(),this.running=!1,this.autoStart=!1}getElapsedTime(){return this.getDelta(),this.elapsedTime}getDelta(){let e=0;if(this.autoStart&&!this.running)return this.start(),0;if(this.running){let t=Su();e=(t-this.oldTime)/1e3,this.oldTime=t,this.elapsedTime+=e}return e}};function Su(){return(typeof performance>"u"?Date:performance).now()}var Jn=new A,bu=new Ut,wx=new A,$n=new A,Eu=class extends Je{constructor(){super(),this.type="AudioListener",this.context=_a.getContext(),this.gain=this.context.createGain(),this.gain.connect(this.context.destination),this.filter=null,this.timeDelta=0,this._clock=new Oc}getInput(){return this.gain}removeFilter(){return this.filter!==null&&(this.gain.disconnect(this.filter),this.filter.disconnect(this.context.destination),this.gain.connect(this.context.destination),this.filter=null),this}getFilter(){return this.filter}setFilter(e){return this.filter!==null?(this.gain.disconnect(this.filter),this.filter.disconnect(this.context.destination)):this.gain.disconnect(this.context.destination),this.filter=e,this.gain.connect(this.filter),this.filter.connect(this.context.destination),this}getMasterVolume(){return this.gain.gain.value}setMasterVolume(e){return this.gain.gain.setTargetAtTime(e,this.context.currentTime,.01),this}updateMatrixWorld(e){super.updateMatrixWorld(e);let t=this.context.listener,n=this.up;if(this.timeDelta=this._clock.getDelta(),this.matrixWorld.decompose(Jn,bu,wx),$n.set(0,0,-1).applyQuaternion(bu),t.positionX){let i=this.context.currentTime+this.timeDelta;t.positionX.linearRampToValueAtTime(Jn.x,i),t.positionY.linearRampToValueAtTime(Jn.y,i),t.positionZ.linearRampToValueAtTime(Jn.z,i),t.forwardX.linearRampToValueAtTime($n.x,i),t.forwardY.linearRampToValueAtTime($n.y,i),t.forwardZ.linearRampToValueAtTime($n.z,i),t.upX.linearRampToValueAtTime(n.x,i),t.upY.linearRampToValueAtTime(n.y,i),t.upZ.linearRampToValueAtTime(n.z,i)}else t.setPosition(Jn.x,Jn.y,Jn.z),t.setOrientation($n.x,$n.y,$n.z,n.x,n.y,n.z)}},Fc=class extends Je{constructor(e){super(),this.type="Audio",this.listener=e,this.context=e.context,this.gain=this.context.createGain(),this.gain.connect(e.getInput()),this.autoplay=!1,this.buffer=null,this.detune=0,this.loop=!1,this.loopStart=0,this.loopEnd=0,this.offset=0,this.duration=void 0,this.playbackRate=1,this.isPlaying=!1,this.hasPlaybackControl=!0,this.source=null,this.sourceType="empty",this._startedAt=0,this._progress=0,this._connected=!1,this.filters=[]}getOutput(){return this.gain}setNodeSource(e){return this.hasPlaybackControl=!1,this.sourceType="audioNode",this.source=e,this.connect(),this}setMediaElementSource(e){return this.hasPlaybackControl=!1,this.sourceType="mediaNode",this.source=this.context.createMediaElementSource(e),this.connect(),this}setMediaStreamSource(e){return this.hasPlaybackControl=!1,this.sourceType="mediaStreamNode",this.source=this.context.createMediaStreamSource(e),this.connect(),this}setBuffer(e){return this.buffer=e,this.sourceType="buffer",this.autoplay&&this.play(),this}play(e=0){if(this.isPlaying===!0){console.warn("THREE.Audio: Audio is already playing.");return}if(this.hasPlaybackControl===!1){console.warn("THREE.Audio: this Audio has no playback control.");return}this._startedAt=this.context.currentTime+e;let t=this.context.createBufferSource();return t.buffer=this.buffer,t.loop=this.loop,t.loopStart=this.loopStart,t.loopEnd=this.loopEnd,t.onended=this.onEnded.bind(this),t.start(this._startedAt,this._progress+this.offset,this.duration),this.isPlaying=!0,this.source=t,this.setDetune(this.detune),this.setPlaybackRate(this.playbackRate),this.connect()}pause(){if(this.hasPlaybackControl===!1){console.warn("THREE.Audio: this Audio has no playback control.");return}return this.isPlaying===!0&&(this._progress+=Math.max(this.context.currentTime-this._startedAt,0)*this.playbackRate,this.loop===!0&&(this._progress=this._progress%(this.duration||this.buffer.duration)),this.source.stop(),this.source.onended=null,this.isPlaying=!1),this}stop(){if(this.hasPlaybackControl===!1){console.warn("THREE.Audio: this Audio has no playback control.");return}return this._progress=0,this.source!==null&&(this.source.stop(),this.source.onended=null),this.isPlaying=!1,this}connect(){if(this.filters.length>0){this.source.connect(this.filters[0]);for(let e=1,t=this.filters.length;e0){this.source.disconnect(this.filters[0]);for(let e=1,t=this.filters.length;e0&&this._mixBufferRegionAdditive(n,i,this._addIndex*t,1,t);for(let c=t,l=t+t;c!==l;++c)if(n[c]!==n[c+t]){o.setValue(n,i);break}}saveOriginalState(){let e=this.binding,t=this.buffer,n=this.valueSize,i=n*this._origIndex;e.getValue(t,i);for(let r=n,a=i;r!==a;++r)t[r]=t[i+r%n];this._setIdentity(),this.cumulativeWeight=0,this.cumulativeWeightAdditive=0}restoreOriginalState(){let e=this.valueSize*3;this.binding.setValue(this.buffer,e)}_setAdditiveIdentityNumeric(){let e=this._addIndex*this.valueSize,t=e+this.valueSize;for(let n=e;n=.5)for(let a=0;a!==r;++a)e[t+a]=e[n+a]}_slerp(e,t,n,i){Ut.slerpFlat(e,t,e,t,e,n,i)}_slerpAdditive(e,t,n,i,r){let a=this._workIndex*r;Ut.multiplyQuaternionsFlat(e,a,e,t,e,n),Ut.slerpFlat(e,t,e,t,e,a,i)}_lerp(e,t,n,i,r){let a=1-i;for(let o=0;o!==r;++o){let c=t+o;e[c]=e[c]*a+e[n+o]*i}}_lerpAdditive(e,t,n,i,r){for(let a=0;a!==r;++a){let o=t+a;e[o]=e[o]+e[n+a]*i}}},Kc="\\[\\]\\.:\\/",Rx=new RegExp("["+Kc+"]","g"),Qc="[^"+Kc+"]",Cx="[^"+Kc.replace("\\.","")+"]",Px=/((?:WC+[\/:])*)/.source.replace("WC",Qc),Lx=/(WCOD+)?/.source.replace("WCOD",Cx),Ix=/(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace("WC",Qc),Ux=/\.(WC+)(?:\[(.+)\])?/.source.replace("WC",Qc),Dx=new RegExp("^"+Px+Lx+Ix+Ux+"$"),Nx=["material","materials","bones","map"],zc=class{constructor(e,t,n){let i=n||Ke.parseTrackName(t);this._targetGroup=e,this._bindings=e.subscribe_(t,i)}getValue(e,t){this.bind();let n=this._targetGroup.nCachedObjects_,i=this._bindings[n];i!==void 0&&i.getValue(e,t)}setValue(e,t){let n=this._bindings;for(let i=this._targetGroup.nCachedObjects_,r=n.length;i!==r;++i)n[i].setValue(e,t)}bind(){let e=this._bindings;for(let t=this._targetGroup.nCachedObjects_,n=e.length;t!==n;++t)e[t].bind()}unbind(){let e=this._bindings;for(let t=this._targetGroup.nCachedObjects_,n=e.length;t!==n;++t)e[t].unbind()}},Ke=class s{constructor(e,t,n){this.path=t,this.parsedPath=n||s.parseTrackName(t),this.node=s.findNode(e,this.parsedPath.nodeName),this.rootNode=e,this.getValue=this._getValue_unbound,this.setValue=this._setValue_unbound}static create(e,t,n){return e&&e.isAnimationObjectGroup?new s.Composite(e,t,n):new s(e,t,n)}static sanitizeNodeName(e){return e.replace(/\s/g,"_").replace(Rx,"")}static parseTrackName(e){let t=Dx.exec(e);if(t===null)throw new Error("PropertyBinding: Cannot parse trackName: "+e);let n={nodeName:t[2],objectName:t[3],objectIndex:t[4],propertyName:t[5],propertyIndex:t[6]},i=n.nodeName&&n.nodeName.lastIndexOf(".");if(i!==void 0&&i!==-1){let r=n.nodeName.substring(i+1);Nx.indexOf(r)!==-1&&(n.nodeName=n.nodeName.substring(0,i),n.objectName=r)}if(n.propertyName===null||n.propertyName.length===0)throw new Error("PropertyBinding: can not parse propertyName from trackName: "+e);return n}static findNode(e,t){if(t===void 0||t===""||t==="."||t===-1||t===e.name||t===e.uuid)return e;if(e.skeleton){let n=e.skeleton.getBoneByName(t);if(n!==void 0)return n}if(e.children){let n=function(r){for(let a=0;a=r){let u=r++,d=e[u];t[d.uuid]=h,e[h]=d,t[l]=u,e[u]=c;for(let f=0,m=i;f!==m;++f){let _=n[f],g=_[u],p=_[h];_[h]=g,_[u]=p}}}this.nCachedObjects_=r}uncache(){let e=this._objects,t=this._indicesByUUID,n=this._bindings,i=n.length,r=this.nCachedObjects_,a=e.length;for(let o=0,c=arguments.length;o!==c;++o){let l=arguments[o],h=l.uuid,u=t[h];if(u!==void 0)if(delete t[h],u0&&(t[f.uuid]=u),e[u]=f,e.pop();for(let m=0,_=i;m!==_;++m){let g=n[m];g[u]=g[d],g.pop()}}}this.nCachedObjects_=r}subscribe_(e,t){let n=this._bindingsIndicesByPath,i=n[e],r=this._bindings;if(i!==void 0)return r[i];let a=this._paths,o=this._parsedPaths,c=this._objects,l=c.length,h=this.nCachedObjects_,u=new Array(l);i=r.length,n[e]=i,a.push(e),o.push(t),r.push(u);for(let d=h,f=c.length;d!==f;++d){let m=c[d];u[d]=new Ke(m,e,t)}return u}unsubscribe_(e){let t=this._bindingsIndicesByPath,n=t[e];if(n!==void 0){let i=this._paths,r=this._parsedPaths,a=this._bindings,o=a.length-1,c=a[o],l=e[o];t[l]=n,a[n]=c,a.pop(),r[n]=r[o],r.pop(),i[n]=i[o],i.pop()}}},Vc=class{constructor(e,t,n=null,i=t.blendMode){this._mixer=e,this._clip=t,this._localRoot=n,this.blendMode=i;let r=t.tracks,a=r.length,o=new Array(a),c={endingStart:zi,endingEnd:zi};for(let l=0;l!==a;++l){let h=r[l].createInterpolant(null);o[l]=h,h.settings=c}this._interpolantSettings=c,this._interpolants=o,this._propertyBindings=new Array(a),this._cacheIndex=null,this._byClipCacheIndex=null,this._timeScaleInterpolant=null,this._weightInterpolant=null,this.loop=Af,this._loopCount=-1,this._startTime=null,this.time=0,this.timeScale=1,this._effectiveTimeScale=1,this.weight=1,this._effectiveWeight=1,this.repetitions=1/0,this.paused=!1,this.enabled=!0,this.clampWhenFinished=!1,this.zeroSlopeAtStart=!0,this.zeroSlopeAtEnd=!0}play(){return this._mixer._activateAction(this),this}stop(){return this._mixer._deactivateAction(this),this.reset()}reset(){return this.paused=!1,this.enabled=!0,this.time=0,this._loopCount=-1,this._startTime=null,this.stopFading().stopWarping()}isRunning(){return this.enabled&&!this.paused&&this.timeScale!==0&&this._startTime===null&&this._mixer._isActiveAction(this)}isScheduled(){return this._mixer._isActiveAction(this)}startAt(e){return this._startTime=e,this}setLoop(e,t){return this.loop=e,this.repetitions=t,this}setEffectiveWeight(e){return this.weight=e,this._effectiveWeight=this.enabled?e:0,this.stopFading()}getEffectiveWeight(){return this._effectiveWeight}fadeIn(e){return this._scheduleFading(e,0,1)}fadeOut(e){return this._scheduleFading(e,1,0)}crossFadeFrom(e,t,n){if(e.fadeOut(t),this.fadeIn(t),n){let i=this._clip.duration,r=e._clip.duration,a=r/i,o=i/r;e.warp(1,a,t),this.warp(o,1,t)}return this}crossFadeTo(e,t,n){return e.crossFadeFrom(this,t,n)}stopFading(){let e=this._weightInterpolant;return e!==null&&(this._weightInterpolant=null,this._mixer._takeBackControlInterpolant(e)),this}setEffectiveTimeScale(e){return this.timeScale=e,this._effectiveTimeScale=this.paused?0:e,this.stopWarping()}getEffectiveTimeScale(){return this._effectiveTimeScale}setDuration(e){return this.timeScale=this._clip.duration/e,this.stopWarping()}syncWith(e){return this.time=e.time,this.timeScale=e.timeScale,this.stopWarping()}halt(e){return this.warp(this._effectiveTimeScale,0,e)}warp(e,t,n){let i=this._mixer,r=i.time,a=this.timeScale,o=this._timeScaleInterpolant;o===null&&(o=i._lendControlInterpolant(),this._timeScaleInterpolant=o);let c=o.parameterPositions,l=o.sampleValues;return c[0]=r,c[1]=r+n,l[0]=e/a,l[1]=t/a,this}stopWarping(){let e=this._timeScaleInterpolant;return e!==null&&(this._timeScaleInterpolant=null,this._mixer._takeBackControlInterpolant(e)),this}getMixer(){return this._mixer}getClip(){return this._clip}getRoot(){return this._localRoot||this._mixer._root}_update(e,t,n,i){if(!this.enabled){this._updateWeight(e);return}let r=this._startTime;if(r!==null){let c=(e-r)*n;c<0||n===0?t=0:(this._startTime=null,t=n*c)}t*=this._updateTimeScale(e);let a=this._updateTime(t),o=this._updateWeight(e);if(o>0){let c=this._interpolants,l=this._propertyBindings;switch(this.blendMode){case xd:for(let h=0,u=c.length;h!==u;++h)c[h].evaluate(a),l[h].accumulateAdditive(o);break;case Xc:default:for(let h=0,u=c.length;h!==u;++h)c[h].evaluate(a),l[h].accumulate(i,o)}}}_updateWeight(e){let t=0;if(this.enabled){t=this.weight;let n=this._weightInterpolant;if(n!==null){let i=n.evaluate(e)[0];t*=i,e>n.parameterPositions[1]&&(this.stopFading(),i===0&&(this.enabled=!1))}}return this._effectiveWeight=t,t}_updateTimeScale(e){let t=0;if(!this.paused){t=this.timeScale;let n=this._timeScaleInterpolant;if(n!==null){let i=n.evaluate(e)[0];t*=i,e>n.parameterPositions[1]&&(this.stopWarping(),t===0?this.paused=!0:this.timeScale=t)}}return this._effectiveTimeScale=t,t}_updateTime(e){let t=this._clip.duration,n=this.loop,i=this.time+e,r=this._loopCount,a=n===Rf;if(e===0)return r===-1?i:a&&(r&1)===1?t-i:i;if(n===wf){r===-1&&(this._loopCount=0,this._setEndings(!0,!0,!1));e:{if(i>=t)i=t;else if(i<0)i=0;else{this.time=i;break e}this.clampWhenFinished?this.paused=!0:this.enabled=!1,this.time=i,this._mixer.dispatchEvent({type:"finished",action:this,direction:e<0?-1:1})}}else{if(r===-1&&(e>=0?(r=0,this._setEndings(!0,this.repetitions===0,a)):this._setEndings(this.repetitions===0,!0,a)),i>=t||i<0){let o=Math.floor(i/t);i-=t*o,r+=Math.abs(o);let c=this.repetitions-r;if(c<=0)this.clampWhenFinished?this.paused=!0:this.enabled=!1,i=e>0?t:0,this.time=i,this._mixer.dispatchEvent({type:"finished",action:this,direction:e>0?1:-1});else{if(c===1){let l=e<0;this._setEndings(l,!l,a)}else this._setEndings(!1,!1,a);this._loopCount=r,this.time=i,this._mixer.dispatchEvent({type:"loop",action:this,loopDelta:o})}}else this.time=i;if(a&&(r&1)===1)return t-i}return i}_setEndings(e,t,n){let i=this._interpolantSettings;n?(i.endingStart=Vi,i.endingEnd=Vi):(e?i.endingStart=this.zeroSlopeAtStart?Vi:zi:i.endingStart=Br,t?i.endingEnd=this.zeroSlopeAtEnd?Vi:zi:i.endingEnd=Br)}_scheduleFading(e,t,n){let i=this._mixer,r=i.time,a=this._weightInterpolant;a===null&&(a=i._lendControlInterpolant(),this._weightInterpolant=a);let o=a.parameterPositions,c=a.sampleValues;return o[0]=r,c[0]=t,o[1]=r+e,c[1]=n,this}},Ox=new Float32Array(1),Cu=class extends sn{constructor(e){super(),this._root=e,this._initMemoryManager(),this._accuIndex=0,this.time=0,this.timeScale=1}_bindAction(e,t){let n=e._localRoot||this._root,i=e._clip.tracks,r=i.length,a=e._propertyBindings,o=e._interpolants,c=n.uuid,l=this._bindingsByRootAndName,h=l[c];h===void 0&&(h={},l[c]=h);for(let u=0;u!==r;++u){let d=i[u],f=d.name,m=h[f];if(m!==void 0)++m.referenceCount,a[u]=m;else{if(m=a[u],m!==void 0){m._cacheIndex===null&&(++m.referenceCount,this._addInactiveBinding(m,c,f));continue}let _=t&&t._propertyBindings[u].binding.parsedPath;m=new Bc(Ke.create(n,f,_),d.ValueTypeName,d.getValueSize()),++m.referenceCount,this._addInactiveBinding(m,c,f),a[u]=m}o[u].resultBuffer=m.buffer}}_activateAction(e){if(!this._isActiveAction(e)){if(e._cacheIndex===null){let n=(e._localRoot||this._root).uuid,i=e._clip.uuid,r=this._actionsByClip[i];this._bindAction(e,r&&r.knownActions[0]),this._addInactiveAction(e,i,n)}let t=e._propertyBindings;for(let n=0,i=t.length;n!==i;++n){let r=t[n];r.useCount++===0&&(this._lendBinding(r),r.saveOriginalState())}this._lendAction(e)}}_deactivateAction(e){if(this._isActiveAction(e)){let t=e._propertyBindings;for(let n=0,i=t.length;n!==i;++n){let r=t[n];--r.useCount===0&&(r.restoreOriginalState(),this._takeBackBinding(r))}this._takeBackAction(e)}}_initMemoryManager(){this._actions=[],this._nActiveActions=0,this._actionsByClip={},this._bindings=[],this._nActiveBindings=0,this._bindingsByRootAndName={},this._controlInterpolants=[],this._nActiveControlInterpolants=0;let e=this;this.stats={actions:{get total(){return e._actions.length},get inUse(){return e._nActiveActions}},bindings:{get total(){return e._bindings.length},get inUse(){return e._nActiveBindings}},controlInterpolants:{get total(){return e._controlInterpolants.length},get inUse(){return e._nActiveControlInterpolants}}}}_isActiveAction(e){let t=e._cacheIndex;return t!==null&&t=0;--n)e[n].stop();return this}update(e){e*=this.timeScale;let t=this._actions,n=this._nActiveActions,i=this.time+=e,r=Math.sign(e),a=this._accuIndex^=1;for(let l=0;l!==n;++l)t[l]._update(i,e,r,a);let o=this._bindings,c=this._nActiveBindings;for(let l=0;l!==c;++l)o[l].apply(a);return this}setTime(e){this.time=0;for(let t=0;tthis.max.x||e.ythis.max.y)}containsBox(e){return this.min.x<=e.min.x&&e.max.x<=this.max.x&&this.min.y<=e.min.y&&e.max.y<=this.max.y}getParameter(e,t){return t.set((e.x-this.min.x)/(this.max.x-this.min.x),(e.y-this.min.y)/(this.max.y-this.min.y))}intersectsBox(e){return!(e.max.xthis.max.x||e.max.ythis.max.y)}clampPoint(e,t){return t.copy(e).clamp(this.min,this.max)}distanceToPoint(e){return this.clampPoint(e,Bu).distanceTo(e)}intersect(e){return this.min.max(e.min),this.max.min(e.max),this.isEmpty()&&this.makeEmpty(),this}union(e){return this.min.min(e.min),this.max.max(e.max),this}translate(e){return this.min.add(e),this.max.add(e),this}equals(e){return e.min.equals(this.min)&&e.max.equals(this.max)}},Vu=new A,Tr=new A,ku=class{constructor(e=new A,t=new A){this.start=e,this.end=t}set(e,t){return this.start.copy(e),this.end.copy(t),this}copy(e){return this.start.copy(e.start),this.end.copy(e.end),this}getCenter(e){return e.addVectors(this.start,this.end).multiplyScalar(.5)}delta(e){return e.subVectors(this.end,this.start)}distanceSq(){return this.start.distanceToSquared(this.end)}distance(){return this.start.distanceTo(this.end)}at(e,t){return this.delta(t).multiplyScalar(e).add(this.start)}closestPointToPointParameter(e,t){Vu.subVectors(e,this.start),Tr.subVectors(this.end,this.start);let n=Tr.dot(Tr),r=Tr.dot(Vu)/n;return t&&(r=ct(r,0,1)),r}closestPointToPoint(e,t,n){let i=this.closestPointToPointParameter(e,t);return this.delta(n).multiplyScalar(i).add(this.start)}applyMatrix4(e){return this.start.applyMatrix4(e),this.end.applyMatrix4(e),this}equals(e){return e.start.equals(this.start)&&e.end.equals(this.end)}clone(){return new this.constructor().copy(this)}},Hu=new A,Gu=class extends Je{constructor(e,t){super(),this.light=e,this.matrix=e.matrixWorld,this.matrixAutoUpdate=!1,this.color=t,this.type="SpotLightHelper";let n=new Ge,i=[0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,-1,0,1,0,0,0,0,1,1,0,0,0,0,-1,1];for(let a=0,o=1,c=32;a1)for(let u=0;u.99999)this.quaternion.set(0,0,0,1);else if(e.y<-.99999)this.quaternion.set(1,0,0,0);else{sd.set(e.z,0,-e.x).normalize();let t=Math.acos(e.y);this.quaternion.setFromAxisAngle(sd,t)}}setLength(e,t=e*.2,n=t*.2){this.line.scale.set(1,Math.max(1e-4,e-t),1),this.line.updateMatrix(),this.cone.scale.set(n,t,n),this.cone.position.y=e,this.cone.updateMatrix()}setColor(e){this.line.material.color.set(e),this.cone.material.color.set(e)}copy(e){return super.copy(e,!1),this.line.copy(e.line),this.cone.copy(e.cone),this}dispose(){this.line.geometry.dispose(),this.line.material.dispose(),this.cone.geometry.dispose(),this.cone.material.dispose()}},ad=class extends en{constructor(e=1){let t=[0,0,0,e,0,0,0,0,0,0,e,0,0,0,0,0,0,e],n=[1,0,0,1,.6,0,0,1,0,.6,1,0,0,0,1,0,.6,1],i=new Ge;i.setAttribute("position",new ve(t,3)),i.setAttribute("color",new ve(n,3));let r=new wt({vertexColors:!0,toneMapped:!1});super(i,r),this.type="AxesHelper"}setColors(e,t,n){let i=new pe,r=this.geometry.attributes.color.array;return i.set(e),i.toArray(r,0),i.toArray(r,3),i.set(t),i.toArray(r,6),i.toArray(r,9),i.set(n),i.toArray(r,12),i.toArray(r,15),this.geometry.attributes.color.needsUpdate=!0,this}dispose(){this.geometry.dispose(),this.material.dispose()}},od=class{constructor(){this.type="ShapePath",this.color=new pe,this.subPaths=[],this.currentPath=null}moveTo(e,t){return this.currentPath=new ji,this.subPaths.push(this.currentPath),this.currentPath.moveTo(e,t),this}lineTo(e,t){return this.currentPath.lineTo(e,t),this}quadraticCurveTo(e,t,n,i){return this.currentPath.quadraticCurveTo(e,t,n,i),this}bezierCurveTo(e,t,n,i,r,a){return this.currentPath.bezierCurveTo(e,t,n,i,r,a),this}splineThru(e){return this.currentPath.splineThru(e),this}toShapes(e){function t(p){let v=[];for(let x=0,y=p.length;xNumber.EPSILON){if(T<0&&(R=v[w],M=-M,I=v[b],T=-T),p.yI.y)continue;if(p.y===R.y){if(p.x===R.x)return!0}else{let O=T*(p.x-R.x)-M*(p.y-R.y);if(O===0)return!0;if(O<0)continue;y=!y}}else{if(p.y!==R.y)continue;if(I.x<=p.x&&p.x<=R.x||R.x<=p.x&&p.x<=I.x)return!0}}return y}let i=yn.isClockWise,r=this.subPaths;if(r.length===0)return[];let a,o,c,l=[];if(r.length===1)return o=r[0],c=new Fn,c.curves=o.curves,l.push(c),l;let h=!i(r[0].getPoints());h=e?!h:h;let u=[],d=[],f=[],m=0,_;d[m]=void 0,f[m]=[];for(let p=0,v=r.length;p1){let p=!1,v=0;for(let x=0,y=d.length;x0&&p===!1&&(f=u)}let g;for(let p=0,v=d.length;p { @@ -229,15 +303,18 @@ function Makie.colorbuffer(screen::Screen) return session2image(three.session, screen.scene) end - function insert_scene!(disp, screen::Screen, scene::Scene) if js_uuid(scene) in screen.displayed_scenes return true else + if !(js_uuid(scene.parent) in screen.displayed_scenes) + # Parents serialize their child scenes, so we only need to + # serialize & update the parent scene + return insert_scene!(disp, screen, scene.parent) + end scene_ser = serialize_scene(scene) parent = scene.parent parent_uuid = js_uuid(parent) - insert_scene!(disp, screen, parent) # make sure parent is also already displayed err = "Cant find scene js_uuid(scene) == $(parent_uuid)" evaljs_value(disp.session, js""" $(WGL).then(WGL=> { @@ -254,14 +331,23 @@ function insert_scene!(disp, screen::Screen, scene::Scene) end end -function Base.insert!(screen::Screen, scene::Scene, plot::Combined) +function insert_plot!(disp::ThreeDisplay, scene::Scene, @nospecialize(plot::Plot)) + plot_data = serialize_plots(scene, [plot]) + plot_sub = Session(disp.session) + JSServe.init_session(plot_sub) + plot.__wgl_session = plot_sub + js = js""" + $(WGL).then(WGL=> { + WGL.insert_plot($(js_uuid(scene)), $plot_data); + })""" + JSServe.evaljs_value(plot_sub, js; timeout=50) + return +end + +function Base.insert!(screen::Screen, scene::Scene, @nospecialize(plot::Plot)) disp = get_three(screen; error="Plot needs to be displayed to insert additional plots") if js_uuid(scene) in screen.displayed_scenes - plot_data = serialize_plots(scene, [plot]) - JSServe.evaljs_value(disp.session, js""" - $(WGL).then(WGL=> { - WGL.insert_plot($(js_uuid(scene)), $plot_data); - })""") + insert_plot!(disp, scene, plot) else # Newly created scene gets inserted! # This must be a child plot of some parent, otherwise a plot wouldn't be inserted via `insert!(screen, ...)` @@ -279,45 +365,90 @@ function Base.insert!(screen::Screen, scene::Scene, plot::Combined) return end -struct LockfreeQueue{T, F} +function delete_js_objects!(screen::Screen, plot_uuids::Vector{String}, + session::Union{Nothing,Session}) + three = get_three(screen) + isnothing(three) && return # if no session we haven't displayed and dont need to delete + isready(three.session) || return + JSServe.evaljs(three.session, js""" + $(WGL).then(WGL=> { + WGL.delete_plots($(plot_uuids)); + })""") + !isnothing(session) && close(session) + return +end + +function all_plots_scenes(scene::Scene; scene_uuids=String[], plots=Plot[]) + push!(scene_uuids, js_uuid(scene)) + append!(plots, scene.plots) + for child in scene.children + all_plots_scenes(child; plots=plots, scene_uuids=scene_uuids) + end + return scene_uuids, plots +end + +function delete_js_objects!(screen::Screen, scene::Scene) + three = get_three(screen) + isnothing(three) && return # if no session we haven't displayed and dont need to delete + isready(three.session) || return + scene_uuids, plots = all_plots_scenes(scene) + for plot in plots + if haskey(plot, :__wgl_session) + session = plot.__wgl_session[] + close(session) + end + end + JSServe.evaljs(three.session, js""" + $(WGL).then(WGL=> { + WGL.delete_scenes($scene_uuids, $(js_uuid.(plots))); + })""") + return +end + + +struct LockfreeQueue{T,F} # Double buffering to be lock free queue1::Vector{T} queue2::Vector{T} current_queue::Threads.Atomic{Int} - task::Base.RefValue{Union{Nothing, Task}} + task::Base.RefValue{Union{Nothing,Task}} execute_job::F end -function LockfreeQueue{T}(execute_job::F) where {T, F} - return LockfreeQueue{T, F}( - T[], - T[], - Threads.Atomic{Int}(1), - Base.RefValue{Union{Nothing, Task}}(nothing), - execute_job - ) +function LockfreeQueue{T}(execute_job::F) where {T,F} + return LockfreeQueue{T,F}(T[], + T[], + Threads.Atomic{Int}(1), + Base.RefValue{Union{Nothing,Task}}(nothing), + execute_job) end function run_jobs!(queue::LockfreeQueue) # already running: !isnothing(queue.task[]) && !istaskdone(queue.task[]) && return - queue.task[] = @async while true - q = nothing - if !isempty(queue.queue1) - queue.current_queue[] = 1 - q = queue.queue1 - elseif !isempty(queue.queue2) - queue.current_queue[] = 2 - q = queue.queue2 - end - if !isnothing(q) - while !isempty(q) - item = pop!(q) - queue.execute_job(item...) + return queue.task[] = @async while true + try + q = nothing + if !isempty(queue.queue1) + queue.current_queue[] = 1 + q = queue.queue1 + elseif !isempty(queue.queue2) + queue.current_queue[] = 2 + q = queue.queue2 + end + if !isnothing(q) + while !isempty(q) + item = pop!(q) + Base.invokelatest(queue.execute_job, item...) + end + end + sleep(0.1) + catch e + if !(e isa EOFError) + @warn "error while running JS objects" exception = (e, Base.catch_backtrace()) end end - sleep(0.1) end end @@ -331,25 +462,24 @@ function Base.push!(queue::LockfreeQueue, item) end end -function delete_plot!(td::Screen, scene::String, uuids::Vector{String}) - three = get_three(td) - isnothing(three) && return # if no session we haven't displayed and dont need to delete - isready(three.session) || return - JSServe.evaljs(three.session, js""" - $(WGL).then(WGL=> { - WGL.delete_plots($(scene), $(uuids)); - })""") - return -end - const DISABLE_JS_FINALZING = Base.RefValue(false) -const DELETE_QUEUE = LockfreeQueue{Tuple{Screen,String,Vector{String}}}(delete_plot!) +const DELETE_QUEUE = LockfreeQueue{Tuple{Screen, Vector{String}, Union{Session, Nothing}}}(delete_js_objects!) +const SCENE_DELETE_QUEUE = LockfreeQueue{Tuple{Screen,Scene}}(delete_js_objects!) -function Base.delete!(screen::Screen, scene::Scene, plot::Combined) - atomics = Makie.collect_atomic_plots(plot) # delete all atomics +function Base.delete!(screen::Screen, scene::Scene, plot::Plot) # only queue atomics to actually delete on js if !DISABLE_JS_FINALZING[] - push!(DELETE_QUEUE, (screen, js_uuid(scene), js_uuid.(atomics))) + plot_uuids = map(js_uuid, Makie.collect_atomic_plots(plot)) + session = to_value(get(plot, :__wgl_session, nothing)) + push!(DELETE_QUEUE, (screen, plot_uuids, session)) + end + return +end + +function Base.delete!(screen::Screen, scene::Scene) + if !DISABLE_JS_FINALZING[] + push!(SCENE_DELETE_QUEUE, (screen, scene)) end + delete!(screen.displayed_scenes, js_uuid(scene)) return end diff --git a/WGLMakie/src/events.jl b/WGLMakie/src/events.jl index 6db9ccfa831..bd129e83d53 100644 --- a/WGLMakie/src/events.jl +++ b/WGLMakie/src/events.jl @@ -54,7 +54,7 @@ function connect_scene_events!(scene::Scene, comm::Observable) @async try @handle msg.mouseposition begin x, y = Float64.((mouseposition...,)) - e.mouseposition[] = (x, size(scene)[2] - y) + e.mouseposition[] = (x, y) end @handle msg.mousedown begin # This can probably be done better from the JS side? diff --git a/WGLMakie/src/imagelike.jl b/WGLMakie/src/imagelike.jl index 1fc9426b94e..e5417f19fe7 100644 --- a/WGLMakie/src/imagelike.jl +++ b/WGLMakie/src/imagelike.jl @@ -6,29 +6,36 @@ using Makie: el32convert, surface_normals, get_dim nothing_or_color(c) = to_color(c) nothing_or_color(c::Nothing) = RGBAf(0, 0, 0, 1) -lift_or(f, x) = f(x) -lift_or(f, x::Observable) = lift(f, x) - function create_shader(mscene::Scene, plot::Surface) # TODO OWN OPTIMIZED SHADER ... Or at least optimize this a bit more ... px, py, pz = plot[1], plot[2], plot[3] grid(x, y, z, trans, space) = Makie.matrix_grid(p-> apply_transform(trans, p, space), x, y, z) - positions = Buffer(lift(grid, px, py, pz, transform_func_obs(plot), get(plot, :space, :data))) - rect = lift(z -> Tesselation(Rect2(0f0, 0f0, 1f0, 1f0), size(z)), pz) - faces = Buffer(lift(r -> decompose(GLTriangleFace, r), rect)) - uv = Buffer(lift(decompose_uv, rect)) - normals = Buffer(lift(surface_normals, px, py, pz)) - per_vertex = Dict(:positions => positions, :faces => faces, :uv => uv, :normals => normals) + # TODO: Use Makie.surface2mesh + ps = lift(grid, plot, px, py, pz, transform_func_obs(plot), get(plot, :space, :data)) + positions = Buffer(ps) + rect = lift(z -> Tesselation(Rect2(0f0, 0f0, 1f0, 1f0), size(z)), plot, pz) + fs = lift(r -> decompose(QuadFace{Int}, r), plot, rect) + fs = map((ps, fs) -> filter(f -> !any(i -> isnan(ps[i]), f), fs), plot, ps, fs) + faces = Buffer(fs) + # This adjusts uvs (compared to decompose_uv) so texture sampling starts at + # the center of a texture pixel rather than the edge, fixing + # https://github.com/MakieOrg/Makie.jl/pull/2598#discussion_r1152552196 + uv = Buffer(lift(plot, rect) do r + Nx, Ny = r.nvertices + f = Vec2f(1 / Nx, 1 / Ny) + [f .* Vec2f(0.5 + i, 0.5 + j) for j in Ny-1:-1:0 for i in 0:Nx-1] + end) + normals = Buffer(lift(Makie.nan_aware_normals, plot, ps, fs)) + per_vertex = Dict(:positions => positions, :faces => faces, :uv => uv, :normals => normals) uniforms = Dict(:uniform_color => color, :color => false) + return draw_mesh(mscene, per_vertex, plot, uniforms) end function create_shader(mscene::Scene, plot::Union{Heatmap, Image}) - minfilter = to_value(get(plot, :interpolate, false)) ? :linear : :nearest mesh = limits_to_uvmesh(plot) - uniforms = Dict( :normals => Vec3f(0), :shading => false, @@ -45,7 +52,7 @@ function create_shader(mscene::Scene, plot::Volume) x, y, z, vol = plot[1], plot[2], plot[3], plot[4] box = GeometryBasics.mesh(Rect3f(Vec3f(0), Vec3f(1))) cam = cameracontrols(mscene) - model2 = lift(plot.model, x, y, z) do m, xyz... + model2 = lift(plot, plot.model, x, y, z) do m, xyz... mi = minimum.(xyz) maxi = maximum.(xyz) w = maxi .- mi @@ -53,20 +60,20 @@ function create_shader(mscene::Scene, plot::Volume) return convert(Mat4f, m) * m2 end - modelinv = lift(inv, model2) - algorithm = lift(x -> Cuint(convert_attribute(x, key"algorithm"())), plot.algorithm) + modelinv = lift(inv, plot, model2) + algorithm = lift(x -> Cuint(convert_attribute(x, key"algorithm"())), plot, plot.algorithm) - diffuse = lift(x -> convert_attribute(x, Key{:diffuse}()), plot.diffuse) - specular = lift(x -> convert_attribute(x, Key{:specular}()), plot.specular) - shininess = lift(x -> convert_attribute(x, Key{:shininess}()), plot.shininess) + diffuse = lift(x -> convert_attribute(x, Key{:diffuse}()), plot, plot.diffuse) + specular = lift(x -> convert_attribute(x, Key{:specular}()), plot, plot.specular) + shininess = lift(x -> convert_attribute(x, Key{:shininess}()), plot, plot.shininess) uniforms = Dict{Symbol, Any}( :modelinv => modelinv, - :isovalue => lift(Float32, plot.isovalue), - :isorange => lift(Float32, plot.isorange), - :absorption => lift(Float32, get(plot, :absorption, Observable(1.0f0))), + :isovalue => lift(Float32, plot, plot.isovalue), + :isorange => lift(Float32, plot, plot.isorange), + :absorption => lift(Float32, plot, get(plot, :absorption, Observable(1.0f0))), :algorithm => algorithm, :diffuse => diffuse, :specular => specular, @@ -75,7 +82,8 @@ function create_shader(mscene::Scene, plot::Volume) :depth_shift => get(plot, :depth_shift, Observable(0.0f0)), # these get filled in later by serialization, but we need them # as dummy values here, so that the correct uniforms are emitted - :lightposition => Vec3f(1), + :light_direction => Vec3f(1), + :light_color => Vec3f(1), :eyeposition => Vec3f(1), :ambient => Vec3f(1), :picking => false, @@ -128,23 +136,22 @@ function limits_to_uvmesh(plot) # TODO, this branch is only hit by Image, but not for Heatmap with stepranges # because convert_arguments converts x/y to Vector{Float32} if px[] isa StepRangeLen && py[] isa StepRangeLen && Makie.is_identity_transform(t) - rect = lift(px, py) do x, y + rect = lift(plot, px, py) do x, y xmin, xmax = extrema(x) ymin, ymax = extrema(y) return Rect2(xmin, ymin, xmax - xmin, ymax - ymin) end - positions = Buffer(lift(rect -> decompose(Point2f, rect), rect)) - faces = Buffer(lift(rect -> decompose(GLTriangleFace, rect), rect)) - uv = Buffer(lift(decompose_uv, rect)) + positions = Buffer(lift(rect -> decompose(Point2f, rect), plot, rect)) + faces = Buffer(lift(rect -> decompose(GLTriangleFace, rect), plot, rect)) + uv = Buffer(lift(decompose_uv, plot, rect)) else function grid(x, y, trans, space) return Makie.matrix_grid(p -> apply_transform(trans, p, space), x, y, zeros(length(x), length(y))) end - resolution = lift((x, y) -> (length(x), length(y)), px, py; ignore_equal_values=true) - positions = Buffer(lift(grid, px, py, t, get(plot, :space, :data))) - faces = Buffer(lift(fast_faces, resolution)) - uv = Buffer(lift(fast_uv, resolution)) + resolution = lift((x, y) -> (length(x), length(y)), plot, px, py; ignore_equal_values=true) + positions = Buffer(lift(grid, plot, px, py, t, get(plot, :space, :data))) + faces = Buffer(lift(fast_faces, plot, resolution)) + uv = Buffer(lift(fast_uv, plot, resolution)) end - vertices = GeometryBasics.meta(positions; uv=uv) return Dict(:positions => positions, :faces => faces, :uv => uv) end diff --git a/WGLMakie/src/lines.jl b/WGLMakie/src/lines.jl index bfdcfc6f158..9066d676374 100644 --- a/WGLMakie/src/lines.jl +++ b/WGLMakie/src/lines.jl @@ -1,81 +1,45 @@ -topoint(x::AbstractVector{Point{N,Float32}}) where {N} = x - -# GRRR STUPID SubArray, with eltype different from getindex(x, 1) -topoint(x::SubArray) = topoint([el for el in x]) - -function topoint(x::AbstractArray{<:Point{N,T}}) where {T,N} - return topoint(Point{N,Float32}.(x)) -end - -function topoint(x::AbstractArray{<:Tuple{P,P}}) where {P<:Point} - return topoint(reinterpret(P, x)) -end - -function create_shader(scene::Scene, plot::Union{Lines,LineSegments}) - # Potentially per instance attributes - positions = lift(plot[1], transform_func_obs(plot), plot.space) do points, trans, space - points = apply_transform(trans, topoint(points), space) - if plot isa LineSegments - return points - else - # Repeat every second point to connect the lines ! - return topoint(TupleView{2, 1}(points)) +function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) + Makie.@converted_attribute plot (linewidth,) + uniforms = Dict( + :model => plot.model, + :object_id => 1, + :depth_shift => plot.depth_shift, + :picking => false, + ) + + color = plot.calculated_colors + if color[] isa Makie.ColorMapping + uniforms[:colormap] = Sampler(color[].colormap) + uniforms[:colorrange] = color[].colorrange_scaled + uniforms[:highclip] = Makie.highclip(color[]) + uniforms[:lowclip] = Makie.lowclip(color[]) + uniforms[:nan_color] = color[].nan_color + color = color[].color_scaled + else + for name in [:nan_color, :highclip, :lowclip] + uniforms[name] = RGBAf(0, 0, 0, 0) end - trans - end - - startr = lift(p -> 1:2:(length(p) - 1), positions) - endr = lift(p -> 2:2:length(p), positions) - - p_start_end = lift(positions) do positions - return (positions[startr[]], positions[endr[]]) end - - per_instance = Dict{Symbol,Any}(:segment_start => Buffer(lift(first, p_start_end)), - :segment_end => Buffer(lift(last, p_start_end))) - uniforms = Dict{Symbol,Any}() - - linewidth = converted_attribute(plot, :linewidth) - cmap = plot.calculated_colors[] - - color = cmap isa Makie.ColorMapping ? cmap.color_scaled : plot.calculated_colors - - for (k, attribute) in [:linewidth => linewidth, :color => color] - attribute = lift(attribute) do x - plot isa LineSegments && return x - # Repeat every second point to connect the lines! - return isscalar(x) ? x : reinterpret(eltype(x), TupleView{2, 1}(x)) - end - if isscalar(attribute) - uniforms[k] = attribute - uniforms[Symbol("$(k)_start")] = attribute - uniforms[Symbol("$(k)_end")] = attribute + points_transformed = lift(apply_transform, plot, transform_func_obs(plot), plot[1], plot.space) + positions = lift(serialize_buffer_attribute, plot, points_transformed) + attributes = Dict{Symbol, Any}(:linepoint => positions) + for (name, attr) in [:color => color, :linewidth => linewidth] + if Makie.is_scalar_attribute(to_value(attr)) + uniforms[Symbol("$(name)_start")] = attr + uniforms[Symbol("$(name)_end")] = attr else - if attribute[] isa AbstractVector{<:Number} && k == :color - @assert cmap isa Makie.ColorMapping - attribute = lift(Makie.numbers_to_colors, attribute, cmap.colormap, identity, - cmap.colorrange_scaled, cmap.lowclip, - cmap.highclip, - cmap.nan_color) - end - per_instance[Symbol("$(k)_start")] = Buffer(lift(x -> x[startr[]], attribute)) - per_instance[Symbol("$(k)_end")] = Buffer(lift(x -> x[endr[]], attribute)) + attributes[name] = lift(serialize_buffer_attribute, plot, attr) end end - - uniforms[:resolution] = to_value(scene.camera.resolution) # updates in JS - - uniforms[:model] = plot.model - uniforms[:depth_shift] = get(plot, :depth_shift, Observable(0f0)) - positions = meta(Point2f[(0, -1), (0, 1), (1, -1), (1, 1)], - uv=Vec2f[(0, 0), (0, 0), (0, 0), (0, 0)]) - instance = GeometryBasics.Mesh(positions, GLTriangleFace[(1, 2, 3), (2, 4, 3)]) - - # id + picking gets filled in JS, needs to be here to emit the correct shader uniforms - uniforms[:picking] = false - uniforms[:object_id] = UInt32(0) - - return InstancedProgram(WebGL(), lasset("line_segments.vert"), - lasset("line_segments.frag"), instance, - VertexArray(; per_instance...), uniforms) + attr = Dict( + :name => string(Makie.plotkey(plot)) * "-" * string(objectid(plot)), + :visible => plot.visible, + :uuid => js_uuid(plot), + :plot_type => plot isa LineSegments ? "linesegments" : "lines", + :cam_space => plot.space[], + :uniforms => serialize_uniforms(uniforms), + :uniform_updater => uniform_updater(plot, uniforms), + :attributes => attributes + ) + return attr end diff --git a/WGLMakie/src/mapbox-lines.vert b/WGLMakie/src/mapbox-lines.vert new file mode 100644 index 00000000000..d2912bc4d67 --- /dev/null +++ b/WGLMakie/src/mapbox-lines.vert @@ -0,0 +1,74 @@ +#version 300 es +precision highp float; +// floor(127 / 2) == 63.0 +// the maximum allowed miter limit is 2.0 at the moment. the extrude normal is +// stored in a byte (-128..127). we scale regular normals up to length 63, but +// there are also "special" normals that have a bigger length (of up to 126 in +// this case). +// #define scale 63.0 +#define EXTRUDE_SCALE 0.015873016 + +in vec2 a_pos_normal; +in vec4 a_data; + +uniform mat4 u_matrix; +uniform mat2 u_pixels_to_tile_units; +uniform vec2 u_units_to_pixels; +uniform lowp float u_device_pixel_ratio; + +out vec2 v_normal; +out vec2 v_width2; +out float v_gamma_scale; + +lowp float floorwidth = 1.0; +mediump float gapwidth = 0.0; +lowp float offset = 0.0; +float width = 1.0; + +void main() { + + // the distance over which the line edge fades out. + // Retina devices need a smaller distance to avoid aliasing. + float ANTIALIASING = 1.0 / u_device_pixel_ratio / 2.0; + + vec2 a_extrude = a_data.xy - 128.0; + float a_direction = mod(a_data.z, 4.0) - 1.0; + vec2 pos = floor(a_pos_normal * 0.5); + + // x is 1 if it's a round cap, 0 otherwise + // y is 1 if the normal points up, and -1 if it points down + // We store these in the least significant bit of a_pos_normal + mediump vec2 normal = a_pos_normal - 2.0 * pos; + normal.y = normal.y * 2.0 - 1.0; + v_normal = normal; + + // these transformations used to be applied in the JS and native code bases. + // moved them into the shader for clarity and simplicity. + gapwidth = gapwidth / 2.0; + float halfwidth = width / 2.0; + offset = -1.0 * offset; + + float inset = gapwidth + (gapwidth > 0.0 ? ANTIALIASING : 0.0); + float outset = gapwidth + halfwidth * (gapwidth > 0.0 ? 2.0 : 1.0) + (halfwidth == 0.0 ? 0.0 : ANTIALIASING); + + // Scale the extrusion vector down to a normal and then up by the line width + // of this vertex. + mediump vec2 dist = outset * a_extrude * EXTRUDE_SCALE; + + // Calculate the offset when drawing a line that is to the side of the actual line. + // We do this by creating a vector that points towards the extrude, but rotate + // it when we're drawing round end points (a_direction = -1 or 1) since their + // extrude vector points in another direction. + mediump float u = 0.5 * a_direction; + mediump float t = 1.0 - abs(u); + mediump vec2 offset2 = offset * a_extrude * EXTRUDE_SCALE * normal.y * mat2(t, -u, u, t); + + vec4 projected_extrude = u_matrix * vec4(dist * u_pixels_to_tile_units, 0.0, 0.0); + gl_Position = u_matrix * vec4(pos + offset2 * u_pixels_to_tile_units, 0.0, 1.0) + projected_extrude; + + + v_gamma_scale = 1.0; + + v_width2 = vec2(outset, inset); + +} diff --git a/WGLMakie/src/meshes.jl b/WGLMakie/src/meshes.jl index 11922e71647..ded9dee2ab7 100644 --- a/WGLMakie/src/meshes.jl +++ b/WGLMakie/src/meshes.jl @@ -3,8 +3,8 @@ function vertexbuffer(x, trans, space) return apply_transform(trans, pos, space) end -function vertexbuffer(x::Observable, p) - return Buffer(lift(vertexbuffer, x, transform_func_obs(p), get(p, :space, :data))) +function vertexbuffer(x::Observable, @nospecialize(p)) + return Buffer(lift(vertexbuffer, p, x, transform_func_obs(p), get(p, :space, :data))) end facebuffer(x) = faces(x) @@ -12,7 +12,7 @@ facebuffer(x::AbstractArray{<:GLTriangleFace}) = x facebuffer(x::Observable) = Buffer(lift(facebuffer, x)) function converted_attribute(plot::AbstractPlot, key::Symbol) - return lift(plot[key]) do value + return lift(plot, plot[key]) do value return convert_attribute(value, Key{key}(), Key{plotkey(plot)}()) end end @@ -21,7 +21,7 @@ function handle_color!(plot, uniforms, buffers, uniform_color_name = :uniform_co color = plot.calculated_colors minfilter = to_value(get(plot, :interpolate, true)) ? :linear : :nearest - convert_text(x) = permute_tex ? lift(permutedims, x) : x + convert_text(x) = permute_tex ? lift(permutedims, plot, x) : x if color[] isa Colorant uniforms[uniform_color_name] = color @@ -55,28 +55,32 @@ function handle_color!(plot, uniforms, buffers, uniform_color_name = :uniform_co return end +lift_or(f, p, x) = f(x) +lift_or(f, @nospecialize(p), x::Observable) = lift(f, p, x) + function draw_mesh(mscene::Scene, per_vertex, plot, uniforms; permute_tex=true) filter!(kv -> !(kv[2] isa Function), uniforms) handle_color!(plot, uniforms, per_vertex; permute_tex=permute_tex) get!(uniforms, :pattern, false) get!(uniforms, :model, plot.model) - get!(uniforms, :lightposition, Vec3f(1)) get!(uniforms, :ambient, Vec3f(1)) + get!(uniforms, :light_direction, Vec3f(1)) + get!(uniforms, :light_color, Vec3f(1)) uniforms[:interpolate_in_fragment_shader] = get(plot, :interpolate_in_fragment_shader, true) - get!(uniforms, :shading, get(plot, :shading, false)) + get!(uniforms, :shading, to_value(get(plot, :shading, NoShading)) != NoShading) - uniforms[:normalmatrix] = map(mscene.camera.view, plot.model) do v, m + uniforms[:normalmatrix] = map(plot.model) do m i = Vec(1, 2, 3) - return transpose(inv(v[i, i] * m[i, i])) + return transpose(inv(m[i, i])) end for key in (:diffuse, :specular, :shininess, :backlight, :depth_shift) if !haskey(uniforms, key) - uniforms[key] = lift_or(x -> convert_attribute(x, Key{key}()), plot[key]) + uniforms[key] = lift_or(x -> convert_attribute(x, Key{key}()), plot, plot[key]) end end if haskey(uniforms, :color) && haskey(per_vertex, :color) @@ -98,7 +102,7 @@ function create_shader(scene::Scene, plot::Makie.Mesh) # Potentially per instance attributes mesh_signal = plot[1] mattributes = GeometryBasics.attributes - get_attribute(mesh, key) = lift(x -> getproperty(x, key), mesh) + get_attribute(mesh, key) = lift(x -> getproperty(x, key), plot, mesh) data = mattributes(mesh_signal[]) uniforms = Dict{Symbol,Any}() diff --git a/WGLMakie/src/particles.jl b/WGLMakie/src/particles.jl index c62a3f3f68b..5c4a3bc681f 100644 --- a/WGLMakie/src/particles.jl +++ b/WGLMakie/src/particles.jl @@ -38,8 +38,9 @@ end const IGNORE_KEYS = Set([ :shading, :overdraw, :rotation, :distancefield, :space, :markerspace, :fxaa, :visible, :transformation, :alpha, :linewidth, :transparency, :marker, - :lightposition, :cycle, :label, :inspector_clear, :inspector_hover, - :inspector_label + :light_direction, :light_color, + :cycle, :label, :inspector_clear, :inspector_hover, + :inspector_label, :axis_cycler ]) function create_shader(scene::Scene, plot::MeshScatter) @@ -49,7 +50,7 @@ function create_shader(scene::Scene, plot::MeshScatter) return k in per_instance_keys && !(isscalar(v[])) end - per_instance[:offset] = apply_transform(transform_func_obs(plot), plot[1], plot.space) + per_instance[:offset] = lift(apply_transform, plot, transform_func_obs(plot), plot[1], plot.space) for (k, v) in per_instance per_instance[k] = Buffer(lift_convert(k, v, plot)) @@ -77,13 +78,19 @@ function create_shader(scene::Scene, plot::MeshScatter) uniform_dict[:depth_shift] = get(plot, :depth_shift, Observable(0f0)) uniform_dict[:backlight] = plot.backlight - get!(uniform_dict, :ambient, Vec3f(1)) + # Make sure these exist + get!(uniform_dict, :ambient, Vec3f(0.1)) + get!(uniform_dict, :diffuse, Vec3f(0.9)) + get!(uniform_dict, :specular, Vec3f(0.3)) + get!(uniform_dict, :shininess, 8f0) + get!(uniform_dict, :light_direction, Vec3f(1)) + get!(uniform_dict, :light_color, Vec3f(1)) # 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 + uniform_dict[:shading] = map(x -> x != NoShading, plot.shading) return InstancedProgram(WebGL(), lasset("particles.vert"), lasset("particles.frag"), instance, VertexArray(; per_instance...), uniform_dict) @@ -114,9 +121,6 @@ function serialize_three(fta::NoDataTextureAtlas) return tex end - - - function scatter_shader(scene::Scene, attributes, plot) # Potentially per instance attributes per_instance_keys = (:pos, :rotations, :markersize, :color, :intensity, @@ -127,19 +131,19 @@ function scatter_shader(scene::Scene, attributes, plot) atlas = wgl_texture_atlas() if haskey(attributes, :marker) font = get(attributes, :font, Observable(Makie.defaultfont())) - marker = lift(attributes[:marker]) do marker + marker = lift(plot, attributes[:marker]) do marker marker isa Makie.FastPixel && return Rect # FastPixel not supported, but same as Rect just slower return Makie.to_spritemarker(marker) end - markersize = lift(Makie.to_2d_scale, attributes[:markersize]) + markersize = lift(Makie.to_2d_scale, plot, attributes[:markersize]) - msize, offset = Makie.marker_attributes(atlas, marker, markersize, font, attributes[:quad_offset]) + msize, offset = Makie.marker_attributes(atlas, marker, markersize, font, attributes[:quad_offset], plot) attributes[:markersize] = msize attributes[:quad_offset] = offset attributes[:uv_offset_width] = Makie.primitive_uv_offset_width(atlas, marker, font) if to_value(marker) isa AbstractMatrix - uniform_dict[:image] = Sampler(lift(el32convert, marker)) + uniform_dict[:image] = Sampler(lift(el32convert, plot, marker)) end end @@ -166,7 +170,9 @@ function scatter_shader(scene::Scene, attributes, plot) if !isnothing(marker) get!(uniform_dict, :shape_type) do - return Makie.marker_to_sdf_shape(marker) + return lift(plot, marker; ignore_equal_values=true) do marker + return Cint(Makie.marker_to_sdf_shape(to_spritemarker(marker))) + end end end @@ -190,6 +196,7 @@ function scatter_shader(scene::Scene, attributes, plot) instance = uv_mesh(Rect2(-0.5f0, -0.5f0, 1f0, 1f0)) # Don't send obs, since it's overwritten in JS to be updated by the camera uniform_dict[:resolution] = to_value(scene.camera.resolution) + uniform_dict[:px_per_unit] = 1f0 # id + picking gets filled in JS, needs to be here to emit the correct shader uniforms uniform_dict[:picking] = false @@ -200,21 +207,15 @@ end function create_shader(scene::Scene, plot::Scatter) # Potentially per instance attributes - per_instance_keys = (:offset, :rotations, :markersize, :color, :intensity, - :quad_offset) - per_instance = filter(plot.attributes.attributes) do (k, v) - return k in per_instance_keys && !(isscalar(v[])) - end attributes = copy(plot.attributes.attributes) space = get(attributes, :space, :data) - cam = scene.camera attributes[:preprojection] = Mat4f(I) # calculate this in JS - attributes[:pos] = apply_transform(transform_func_obs(plot), plot[1], space) + attributes[:pos] = lift(apply_transform, plot, transform_func_obs(plot), plot[1], space) quad_offset = get(attributes, :marker_offset, Observable(Vec2f(0))) attributes[:marker_offset] = Vec3f(0) attributes[:quad_offset] = quad_offset - attributes[:billboard] = map(rot -> isa(rot, Billboard), plot.rotations) + attributes[:billboard] = lift(rot -> isa(rot, Billboard), plot, plot.rotations) attributes[:model] = plot.model attributes[:depth_shift] = get(plot, :depth_shift, Observable(0f0)) @@ -236,16 +237,16 @@ function create_shader(scene::Scene, plot::Makie.Text{<:Tuple{<:Union{<:Makie.Gl offset = plot.offset atlas = wgl_texture_atlas() - glyph_data = map(pos, glyphcollection, offset, transfunc, space; ignore_equal_values=true) do pos, gc, offset, transfunc, space + glyph_data = lift(plot, pos, glyphcollection, offset, transfunc, space; ignore_equal_values=true) do pos, gc, offset, transfunc, space Makie.text_quads(atlas, pos, to_value(gc), offset, transfunc, space) end # unpack values from the one signal: positions, char_offset, quad_offset, uv_offset_width, scale = map((1, 2, 3, 4, 5)) do i - lift(getindex, glyph_data, i) + return lift(getindex, plot, glyph_data, i) end - uniform_color = lift(glyphcollection) do gc + uniform_color = lift(plot, glyphcollection) do gc if gc isa AbstractArray reduce(vcat, (Makie.collect_vector(g.colors, length(g.glyphs)) for g in gc), init = RGBAf[]) @@ -254,7 +255,7 @@ function create_shader(scene::Scene, plot::Makie.Text{<:Tuple{<:Union{<:Makie.Gl end end - uniform_rotation = lift(glyphcollection) do gc + uniform_rotation = lift(plot, glyphcollection) do gc if gc isa AbstractArray reduce(vcat, (Makie.collect_vector(g.rotations, length(g.glyphs)) for g in gc), init = Quaternionf[]) diff --git a/WGLMakie/src/picking.jl b/WGLMakie/src/picking.jl index 4c5589ffe20..1bbf590fdf2 100644 --- a/WGLMakie/src/picking.jl +++ b/WGLMakie/src/picking.jl @@ -48,6 +48,7 @@ end function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) xy_vec = Cint[round.(Cint, xy)...] range = round(Int, range) + session = get_three(screen; error="Can't do picking!").session selection = JSServe.evaljs_value(session, js""" Promise.all([$(WGL), $(scene)]).then(([WGL, scene]) => WGL.pick_sorted(scene, $(xy_vec), $(range))) diff --git a/WGLMakie/src/precompiles.jl b/WGLMakie/src/precompiles.jl index ea89684a234..0822ed9f3b8 100644 --- a/WGLMakie/src/precompiles.jl +++ b/WGLMakie/src/precompiles.jl @@ -10,7 +10,7 @@ macro compile(block) # So we just do all parts of the stack we can do without browser scene = Makie.get_scene(figlike) session = Session(JSServe.NoConnection(); asset_server=JSServe.NoServer()) - three_display(session, scene) + three_display(Screen(scene), session, scene) JSServe.jsrender(session, figlike) s = serialize_scene(scene) JSServe.SerializedMessage(session, Dict(:data => s)) diff --git a/WGLMakie/src/serialization.jl b/WGLMakie/src/serialization.jl index 3af52d3b034..bee88140232 100644 --- a/WGLMakie/src/serialization.jl +++ b/WGLMakie/src/serialization.jl @@ -192,10 +192,10 @@ function serialize_named_buffer(buffer) end) end -function register_geometry_updates(update_buffer::Observable, named_buffers) +function register_geometry_updates(@nospecialize(plot), update_buffer::Observable, named_buffers) for (name, buffer) in _pairs(named_buffers) if buffer isa Buffer - on(ShaderAbstractions.updater(buffer).update) do (f, args) + on(plot, ShaderAbstractions.updater(buffer).update) do (f, args) # update to replace the whole buffer! if f === ShaderAbstractions.update! new_array = args[1] @@ -209,19 +209,19 @@ function register_geometry_updates(update_buffer::Observable, named_buffers) return update_buffer end -function register_geometry_updates(update_buffer::Observable, program::Program) - return register_geometry_updates(update_buffer, program.vertexarray) +function register_geometry_updates(@nospecialize(plot), update_buffer::Observable, program::Program) + return register_geometry_updates(plot, update_buffer, program.vertexarray) end -function register_geometry_updates(update_buffer::Observable, program::InstancedProgram) - return register_geometry_updates(update_buffer, program.per_instance) +function register_geometry_updates(@nospecialize(plot), update_buffer::Observable, program::InstancedProgram) + return register_geometry_updates(plot, update_buffer, program.per_instance) end -function uniform_updater(uniforms::Dict) +function uniform_updater(@nospecialize(plot), uniforms::Dict) updater = Observable(Any[:none, []]) for (name, value) in uniforms if value isa Sampler - on(ShaderAbstractions.updater(value).update) do (f, args) + on(plot, ShaderAbstractions.updater(value).update) do (f, args) if f === ShaderAbstractions.update! updater[] = [name, [Int32[size(value.data)...], serialize_three(args[1])]] end @@ -229,7 +229,7 @@ function uniform_updater(uniforms::Dict) end else value isa Observable || continue - on(value) do value + on(plot, value) do value updater[] = [name, serialize_three(value)] return end @@ -238,53 +238,53 @@ function uniform_updater(uniforms::Dict) return updater end -function serialize_three(ip::InstancedProgram) - program = serialize_three(ip.program) +function serialize_three(@nospecialize(plot), ip::InstancedProgram) + program = serialize_three(plot, ip.program) program[:instance_attributes] = serialize_named_buffer(ip.per_instance) - register_geometry_updates(program[:attribute_updater], ip) + register_geometry_updates(plot, program[:attribute_updater], ip) return program end -reinterpret_faces(faces::AbstractVector) = collect(reinterpret(UInt32, decompose(GLTriangleFace, faces))) +reinterpret_faces(p, faces::AbstractVector) = collect(reinterpret(UInt32, decompose(GLTriangleFace, faces))) -function reinterpret_faces(faces::Buffer) - result = Observable(reinterpret_faces(ShaderAbstractions.data(faces))) - on(ShaderAbstractions.updater(faces).update) do (f, args) +function reinterpret_faces(@nospecialize(plot), faces::Buffer) + result = Observable(reinterpret_faces(plot, ShaderAbstractions.data(faces))) + on(plot, ShaderAbstractions.updater(faces).update) do (f, args) if f === ShaderAbstractions.update! - result[] = reinterpret_faces(args[1]) + result[] = reinterpret_faces(plot, args[1]) end end return result end -function serialize_three(program::Program) - facies = reinterpret_faces(_faces(program.vertexarray)) +function serialize_three(@nospecialize(plot), program::Program) + facies = reinterpret_faces(plot, _faces(program.vertexarray)) indices = convert(Observable, facies) uniforms = serialize_uniforms(program.uniforms) attribute_updater = Observable(["", [], 0]) - register_geometry_updates(attribute_updater, program) + register_geometry_updates(plot, attribute_updater, program) # TODO, make this configurable in ShaderAbstractions update_shader(x) = replace(x, "#version 300 es" => "") return Dict(:vertexarrays => serialize_named_buffer(program.vertexarray), :faces => indices, :uniforms => uniforms, :vertex_source => update_shader(program.vertex_source), :fragment_source => update_shader(program.fragment_source), - :uniform_updater => uniform_updater(program.uniforms), + :uniform_updater => uniform_updater(plot, program.uniforms), :attribute_updater => attribute_updater) end function serialize_scene(scene::Scene) hexcolor(c) = "#" * hex(Colors.color(to_color(c))) - pixel_area = lift(area -> Int32[minimum(area)..., widths(area)...], pixelarea(scene)) + pixel_area = lift(area -> Int32[minimum(area)..., widths(area)...], scene, viewport(scene)) cam_controls = cameracontrols(scene) cam3d_state = if cam_controls isa Camera3D fields = (:lookat, :upvector, :eyeposition, :fov, :near, :far) - dict = Dict((f => serialize_three(getfield(cam_controls, f)[]) for f in fields)) - dict[:resolution] = lift(res -> Int32[res...], scene.camera.resolution) + dict = Dict((f => lift(serialize_three, scene, getfield(cam_controls, f)) for f in fields)) + dict[:resolution] = lift(res -> Int32[res...], scene, scene.camera.resolution) dict else nothing @@ -292,10 +292,17 @@ function serialize_scene(scene::Scene) children = map(child-> serialize_scene(child), scene.children) - serialized = Dict(:pixelarea => pixel_area, - :backgroundcolor => lift(hexcolor, scene.backgroundcolor), + dirlight = Makie.get_directional_light(scene) + light_dir = isnothing(dirlight) ? Observable(Vec3f(1)) : dirlight.direction + cam_rel = isnothing(dirlight) ? false : dirlight.camera_relative + + serialized = Dict(:viewport => pixel_area, + :backgroundcolor => lift(hexcolor, scene, scene.backgroundcolor), + :backgroundcolor_alpha => lift(Colors.alpha, scene, scene.backgroundcolor), :clearscene => scene.clear, :camera => serialize_camera(scene), + :light_direction => light_dir, + :camera_relative_light => cam_rel, :plots => serialize_plots(scene, scene.plots), :cam3d_state => cam3d_state, :visible => scene.visible, @@ -304,8 +311,9 @@ function serialize_scene(scene::Scene) return serialized end -function serialize_plots(scene::Scene, plots::Vector{T}, result=[]) where {T<:AbstractPlot} +function serialize_plots(scene::Scene, @nospecialize(plots::Vector{T}), result=[]) where {T<:AbstractPlot} for plot in plots + plot isa Makie.PlotList && continue # if no plots inserted, this truely is an atomic if isempty(plot.plots) plot_data = serialize_three(scene, plot) @@ -318,9 +326,9 @@ function serialize_plots(scene::Scene, plots::Vector{T}, result=[]) where {T<:Ab return result end -function serialize_three(scene::Scene, plot::AbstractPlot) +function serialize_three(scene::Scene, @nospecialize(plot::AbstractPlot)) program = create_shader(scene, plot) - mesh = serialize_three(program) + mesh = serialize_three(plot, program) mesh[:name] = string(Makie.plotkey(plot)) * "-" * string(objectid(plot)) mesh[:visible] = plot.visible mesh[:uuid] = js_uuid(plot) @@ -330,11 +338,11 @@ function serialize_three(scene::Scene, plot::AbstractPlot) uniforms = mesh[:uniforms] updater = mesh[:uniform_updater] - pointlight = Makie.get_point_light(scene) - if !isnothing(pointlight) - uniforms[:lightposition] = serialize_three(pointlight.position[]) - on(pointlight.position) do value - updater[] = [:lightposition, serialize_three(value)] + dirlight = Makie.get_directional_light(scene) + if !isnothing(dirlight) + uniforms[:light_color] = serialize_three(dirlight.color[]) + on(plot, dirlight.color) do value + updater[] = [:light_color, serialize_three(value)] return end end @@ -342,7 +350,7 @@ function serialize_three(scene::Scene, plot::AbstractPlot) ambientlight = Makie.get_ambient_light(scene) if !isnothing(ambientlight) uniforms[:ambient] = serialize_three(ambientlight.color[]) - on(ambientlight.color) do value + on(plot, ambientlight.color) do value updater[] = [:ambient, serialize_three(value)] return end diff --git a/WGLMakie/src/three_plot.jl b/WGLMakie/src/three_plot.jl index 7ee078b9766..539ff7109bb 100644 --- a/WGLMakie/src/three_plot.jl +++ b/WGLMakie/src/three_plot.jl @@ -3,17 +3,6 @@ # We use objectid to find objects on the js side js_uuid(object) = string(objectid(object)) -function all_plots_scenes(scene::Scene; scene_uuids=String[], plot_uuids=String[]) - push!(scene_uuids, js_uuid(scene)) - for plot in scene.plots - append!(plot_uuids, (js_uuid(p) for p in Makie.collect_atomic_plots(plot))) - end - for child in scene.children - all_plots_scenes(child, plot_uuids=plot_uuids, scene_uuids=scene_uuids) - end - return scene_uuids, plot_uuids -end - function JSServe.print_js_code(io::IO, plot::AbstractPlot, context::JSServe.JSSourceContext) uuids = js_uuid.(Makie.collect_atomic_plots(plot)) # This is a bit more complicated then it has to be, since evaljs / on_document_load @@ -38,13 +27,12 @@ function JSServe.print_js_code(io::IO, scene::Scene, context::JSServe.JSSourceCo JSServe.print_js_code(io, js"""$(WGL).then(WGL=> WGL.find_scene($(js_uuid(scene))))""", context) end -function three_display(session::Session, scene::Scene; screen_config...) - config = Makie.merge_screen_config(ScreenConfig, screen_config)::ScreenConfig +function three_display(screen::Screen, session::Session, scene::Scene) + config = screen.config scene_serialized = serialize_scene(scene) - window_open = scene.events.window_open width, height = size(scene) - canvas_width = lift(x -> [round.(Int, widths(x))...], pixelarea(scene)) + canvas_width = lift(x -> [round.(Int, widths(x))...], scene, viewport(scene)) canvas = DOM.m("canvas"; tabindex="0", style="display: block") wrapper = DOM.div(canvas; style="width: 100%; height: 100%") comm = Observable(Dict{String,Any}()) @@ -56,7 +44,8 @@ function three_display(session::Session, scene::Scene; screen_config...) try { const renderer = WGL.create_scene( $wrapper, $canvas, $canvas_width, $scene_serialized, $comm, $width, $height, - $(ta), $(config.framerate), $(config.resize_to_body)) + $(ta), $(config.framerate), $(config.resize_to), $(config.px_per_unit), $(config.scalefactor) + ) const gl = renderer.getContext() const err = gl.getError() if (err != gl.NO_ERROR) { @@ -77,3 +66,4 @@ function three_display(session::Session, scene::Scene; screen_config...) three = ThreeDisplay(session) return three, wrapper, done_init end +| diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index 0c7d86e9422..cd50663ea6c 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -20195,36 +20195,830 @@ function getErrorMessage(version) { element.innerHTML = message; return element; } -const pixelRatio = window.devicePixelRatio || 1.0; -function event2scene_pixel(scene, event) { - const { canvas } = scene.screen; +function typedarray_to_vectype(typedArray, ndim) { + if (ndim === 1) { + return "float"; + } else if (typedArray instanceof Float32Array) { + return "vec" + ndim; + } else if (typedArray instanceof Int32Array) { + return "ivec" + ndim; + } else if (typedArray instanceof Uint32Array) { + return "uvec" + ndim; + } else { + return; + } +} +function attribute_type(attribute) { + if (attribute) { + return typedarray_to_vectype(attribute.array, attribute.itemSize); + } else { + return; + } +} +function uniform_type(obj) { + if (obj instanceof THREE.Uniform) { + return uniform_type(obj.value); + } else if (typeof obj === "number") { + return "float"; + } else if (typeof obj === "boolean") { + return "bool"; + } else if (obj instanceof THREE.Vector2) { + return "vec2"; + } else if (obj instanceof THREE.Vector3) { + return "vec3"; + } else if (obj instanceof THREE.Vector4) { + return "vec4"; + } else if (obj instanceof THREE.Color) { + return "vec4"; + } else if (obj instanceof THREE.Matrix3) { + return "mat3"; + } else if (obj instanceof THREE.Matrix4) { + return "mat4"; + } else if (obj instanceof THREE.Texture) { + return "sampler2D"; + } else { + return; + } +} +function uniforms_to_type_declaration(uniform_dict) { + let result = ""; + for(const name in uniform_dict){ + const uniform = uniform_dict[name]; + const type = uniform_type(uniform); + result += `uniform ${type} ${name};\n`; + } + return result; +} +function attributes_to_type_declaration(attributes_dict) { + let result = ""; + for(const name in attributes_dict){ + const attribute = attributes_dict[name]; + const type = attribute_type(attribute); + result += `in ${type} ${name};\n`; + } + return result; +} +const _changeEvent = { + type: "change" +}; +const _startEvent = { + type: "start" +}; +const _endEvent = { + type: "end" +}; +const _ray = new hi(); +const _plane = new mn(); +const TILT_LIMIT = Math.cos(70 * yv.DEG2RAD); +class OrbitControls extends sn { + constructor(object, domElement, allow_update, is_in_scene){ + super(); + this.object = object; + this.domElement = domElement; + this.domElement.style.touchAction = "none"; + this.enabled = true; + this.target = new A(); + this.cursor = new A(); + this.minDistance = 0; + this.maxDistance = Infinity; + this.minZoom = 0; + this.maxZoom = Infinity; + this.minTargetRadius = 0; + this.maxTargetRadius = Infinity; + this.minPolarAngle = 0; + this.maxPolarAngle = Math.PI; + this.minAzimuthAngle = -Infinity; + this.maxAzimuthAngle = Infinity; + this.enableDamping = false; + this.dampingFactor = 0.05; + this.enableZoom = true; + this.zoomSpeed = 1.0; + this.enableRotate = true; + this.rotateSpeed = 1.0; + this.enablePan = true; + this.panSpeed = 1.0; + this.screenSpacePanning = true; + this.keyPanSpeed = 7.0; + this.zoomToCursor = false; + this.autoRotate = false; + this.autoRotateSpeed = 2.0; + this.keys = { + LEFT: "ArrowLeft", + UP: "ArrowUp", + RIGHT: "ArrowRight", + BOTTOM: "ArrowDown" + }; + this.mouseButtons = { + LEFT: zx.ROTATE, + MIDDLE: zx.DOLLY, + RIGHT: zx.PAN + }; + this.touches = { + ONE: Vx.ROTATE, + TWO: Vx.DOLLY_PAN + }; + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; + this._domElementKeyEvents = null; + this.getPolarAngle = function() { + return spherical.phi; + }; + this.getAzimuthalAngle = function() { + return spherical.theta; + }; + this.getDistance = function() { + return this.object.position.distanceTo(this.target); + }; + this.listenToKeyEvents = function(domElement) { + domElement.addEventListener("keydown", onKeyDown); + this._domElementKeyEvents = domElement; + }; + this.stopListenToKeyEvents = function() { + this._domElementKeyEvents.removeEventListener("keydown", onKeyDown); + this._domElementKeyEvents = null; + }; + this.saveState = function() { + scope.target0.copy(scope.target); + scope.position0.copy(scope.object.position); + scope.zoom0 = scope.object.zoom; + }; + this.reset = function() { + scope.target.copy(scope.target0); + scope.object.position.copy(scope.position0); + scope.object.zoom = scope.zoom0; + scope.object.updateProjectionMatrix(); + scope.dispatchEvent(_changeEvent); + scope.update(); + state = STATE.NONE; + }; + this.update = function() { + const offset = new A(); + const quat = new Ut().setFromUnitVectors(object.up, new A(0, 1, 0)); + const quatInverse = quat.clone().invert(); + const lastPosition = new A(); + const lastQuaternion = new Ut(); + const lastTargetPosition = new A(); + const twoPI = 2 * Math.PI; + return function update(deltaTime = null) { + if (!allow_update()) { + return; + } + const position = scope.object.position; + offset.copy(position).sub(scope.target); + offset.applyQuaternion(quat); + spherical.setFromVector3(offset); + if (scope.autoRotate && state === STATE.NONE) { + rotateLeft(getAutoRotationAngle(deltaTime)); + } + if (scope.enableDamping) { + spherical.theta += sphericalDelta.theta * scope.dampingFactor; + spherical.phi += sphericalDelta.phi * scope.dampingFactor; + } else { + spherical.theta += sphericalDelta.theta; + spherical.phi += sphericalDelta.phi; + } + let min = scope.minAzimuthAngle; + let max = scope.maxAzimuthAngle; + if (isFinite(min) && isFinite(max)) { + if (min < -Math.PI) min += twoPI; + else if (min > Math.PI) min -= twoPI; + if (max < -Math.PI) max += twoPI; + else if (max > Math.PI) max -= twoPI; + if (min <= max) { + spherical.theta = Math.max(min, Math.min(max, spherical.theta)); + } else { + spherical.theta = spherical.theta > (min + max) / 2 ? Math.max(min, spherical.theta) : Math.min(max, spherical.theta); + } + } + spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); + spherical.makeSafe(); + if (scope.enableDamping === true) { + scope.target.addScaledVector(panOffset, scope.dampingFactor); + } else { + scope.target.add(panOffset); + } + scope.target.sub(scope.cursor); + scope.target.clampLength(scope.minTargetRadius, scope.maxTargetRadius); + scope.target.add(scope.cursor); + if (scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera) { + spherical.radius = clampDistance(spherical.radius); + } else { + spherical.radius = clampDistance(spherical.radius * scale); + } + offset.setFromSpherical(spherical); + offset.applyQuaternion(quatInverse); + position.copy(scope.target).add(offset); + scope.object.lookAt(scope.target); + if (scope.enableDamping === true) { + sphericalDelta.theta *= 1 - scope.dampingFactor; + sphericalDelta.phi *= 1 - scope.dampingFactor; + panOffset.multiplyScalar(1 - scope.dampingFactor); + } else { + sphericalDelta.set(0, 0, 0); + panOffset.set(0, 0, 0); + } + let zoomChanged = false; + if (scope.zoomToCursor && performCursorZoom) { + let newRadius = null; + if (scope.object.isPerspectiveCamera) { + const prevRadius = offset.length(); + newRadius = clampDistance(prevRadius * scale); + const radiusDelta = prevRadius - newRadius; + scope.object.position.addScaledVector(dollyDirection, radiusDelta); + scope.object.updateMatrixWorld(); + } else if (scope.object.isOrthographicCamera) { + const mouseBefore = new A(mouse.x, mouse.y, 0); + mouseBefore.unproject(scope.object); + scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / scale)); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + const mouseAfter = new A(mouse.x, mouse.y, 0); + mouseAfter.unproject(scope.object); + scope.object.position.sub(mouseAfter).add(mouseBefore); + scope.object.updateMatrixWorld(); + newRadius = offset.length(); + } else { + console.warn("WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled."); + scope.zoomToCursor = false; + } + if (newRadius !== null) { + if (this.screenSpacePanning) { + scope.target.set(0, 0, -1).transformDirection(scope.object.matrix).multiplyScalar(newRadius).add(scope.object.position); + } else { + _ray.origin.copy(scope.object.position); + _ray.direction.set(0, 0, -1).transformDirection(scope.object.matrix); + if (Math.abs(scope.object.up.dot(_ray.direction)) < TILT_LIMIT) { + object.lookAt(scope.target); + } else { + _plane.setFromNormalAndCoplanarPoint(scope.object.up, scope.target); + _ray.intersectPlane(_plane, scope.target); + } + } + } + } else if (scope.object.isOrthographicCamera) { + scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / scale)); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + } + scale = 1; + performCursorZoom = false; + if (zoomChanged || lastPosition.distanceToSquared(scope.object.position) > EPS || 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS || lastTargetPosition.distanceToSquared(scope.target) > 0) { + scope.dispatchEvent(_changeEvent); + lastPosition.copy(scope.object.position); + lastQuaternion.copy(scope.object.quaternion); + lastTargetPosition.copy(scope.target); + zoomChanged = false; + return true; + } + return false; + }; + }(); + this.dispose = function() { + scope.domElement.removeEventListener("contextmenu", onContextMenu); + scope.domElement.removeEventListener("pointerdown", onPointerDown); + scope.domElement.removeEventListener("pointercancel", onPointerUp); + scope.domElement.removeEventListener("wheel", onMouseWheel); + scope.domElement.removeEventListener("pointermove", onPointerMove); + scope.domElement.removeEventListener("pointerup", onPointerUp); + if (scope._domElementKeyEvents !== null) { + scope._domElementKeyEvents.removeEventListener("keydown", onKeyDown); + scope._domElementKeyEvents = null; + } + }; + const scope = this; + const STATE = { + NONE: -1, + ROTATE: 0, + DOLLY: 1, + PAN: 2, + TOUCH_ROTATE: 3, + TOUCH_PAN: 4, + TOUCH_DOLLY_PAN: 5, + TOUCH_DOLLY_ROTATE: 6 + }; + let state = STATE.NONE; + const EPS = 0.000001; + const spherical = new Ou(); + const sphericalDelta = new Ou(); + let scale = 1; + const panOffset = new A(); + const rotateStart = new Z(); + const rotateEnd = new Z(); + const rotateDelta = new Z(); + const panStart = new Z(); + const panEnd = new Z(); + const panDelta = new Z(); + const dollyStart = new Z(); + const dollyEnd = new Z(); + const dollyDelta = new Z(); + const dollyDirection = new A(); + const mouse = new Z(); + let performCursorZoom = false; + const pointers = []; + const pointerPositions = {}; + function getAutoRotationAngle(deltaTime) { + if (deltaTime !== null) { + return 2 * Math.PI / 60 * scope.autoRotateSpeed * deltaTime; + } else { + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; + } + } + function getZoomScale() { + return Math.pow(0.95, scope.zoomSpeed); + } + function rotateLeft(angle) { + sphericalDelta.theta -= angle; + } + function rotateUp(angle) { + sphericalDelta.phi -= angle; + } + const panLeft = function() { + const v = new A(); + return function panLeft(distance, objectMatrix) { + v.setFromMatrixColumn(objectMatrix, 0); + v.multiplyScalar(-distance); + panOffset.add(v); + }; + }(); + const panUp = function() { + const v = new A(); + return function panUp(distance, objectMatrix) { + if (scope.screenSpacePanning === true) { + v.setFromMatrixColumn(objectMatrix, 1); + } else { + v.setFromMatrixColumn(objectMatrix, 0); + v.crossVectors(scope.object.up, v); + } + v.multiplyScalar(distance); + panOffset.add(v); + }; + }(); + const pan = function() { + const offset = new A(); + return function pan(deltaX, deltaY) { + const element = scope.domElement; + if (scope.object.isPerspectiveCamera) { + const position = scope.object.position; + offset.copy(position).sub(scope.target); + let targetDistance = offset.length(); + targetDistance *= Math.tan(scope.object.fov / 2 * Math.PI / 180.0); + panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix); + panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix); + } else if (scope.object.isOrthographicCamera) { + panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, scope.object.matrix); + panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope.object.matrix); + } else { + console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."); + scope.enablePan = false; + } + }; + }(); + function dollyOut(dollyScale) { + if (scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera) { + scale /= dollyScale; + } else { + console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."); + scope.enableZoom = false; + } + } + function dollyIn(dollyScale) { + if (scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera) { + scale *= dollyScale; + } else { + console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."); + scope.enableZoom = false; + } + } + function updateMouseParameters(event) { + if (!scope.zoomToCursor) { + return; + } + performCursorZoom = true; + const rect = scope.domElement.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + const w = rect.width; + const h = rect.height; + mouse.x = x / w * 2 - 1; + mouse.y = -(y / h) * 2 + 1; + dollyDirection.set(mouse.x, mouse.y, 1).unproject(scope.object).sub(scope.object.position).normalize(); + } + function clampDistance(dist) { + return Math.max(scope.minDistance, Math.min(scope.maxDistance, dist)); + } + function handleMouseDownRotate(event) { + rotateStart.set(event.clientX, event.clientY); + } + function handleMouseDownDolly(event) { + updateMouseParameters(event); + dollyStart.set(event.clientX, event.clientY); + } + function handleMouseDownPan(event) { + panStart.set(event.clientX, event.clientY); + } + function handleMouseMoveRotate(event) { + rotateEnd.set(event.clientX, event.clientY); + rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); + const element = scope.domElement; + rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); + rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); + rotateStart.copy(rotateEnd); + scope.update(); + } + function handleMouseMoveDolly(event) { + dollyEnd.set(event.clientX, event.clientY); + dollyDelta.subVectors(dollyEnd, dollyStart); + if (dollyDelta.y > 0) { + dollyOut(getZoomScale()); + } else if (dollyDelta.y < 0) { + dollyIn(getZoomScale()); + } + dollyStart.copy(dollyEnd); + scope.update(); + } + function handleMouseMovePan(event) { + panEnd.set(event.clientX, event.clientY); + panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); + pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + scope.update(); + } + function handleMouseWheel(event) { + updateMouseParameters(event); + if (event.deltaY < 0) { + dollyIn(getZoomScale()); + } else if (event.deltaY > 0) { + dollyOut(getZoomScale()); + } + scope.update(); + } + function handleKeyDown(event) { + let needsUpdate = false; + switch(event.code){ + case scope.keys.UP: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateUp(2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight); + } else { + pan(0, scope.keyPanSpeed); + } + needsUpdate = true; + break; + case scope.keys.BOTTOM: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateUp(-2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight); + } else { + pan(0, -scope.keyPanSpeed); + } + needsUpdate = true; + break; + case scope.keys.LEFT: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateLeft(2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight); + } else { + pan(scope.keyPanSpeed, 0); + } + needsUpdate = true; + break; + case scope.keys.RIGHT: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateLeft(-2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight); + } else { + pan(-scope.keyPanSpeed, 0); + } + needsUpdate = true; + break; + } + if (needsUpdate) { + event.preventDefault(); + scope.update(); + } + } + function handleTouchStartRotate() { + if (pointers.length === 1) { + rotateStart.set(pointers[0].pageX, pointers[0].pageY); + } else { + const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); + const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); + rotateStart.set(x, y); + } + } + function handleTouchStartPan() { + if (pointers.length === 1) { + panStart.set(pointers[0].pageX, pointers[0].pageY); + } else { + const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); + const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); + panStart.set(x, y); + } + } + function handleTouchStartDolly() { + const dx = pointers[0].pageX - pointers[1].pageX; + const dy = pointers[0].pageY - pointers[1].pageY; + const distance = Math.sqrt(dx * dx + dy * dy); + dollyStart.set(0, distance); + } + function handleTouchStartDollyPan() { + if (scope.enableZoom) handleTouchStartDolly(); + if (scope.enablePan) handleTouchStartPan(); + } + function handleTouchStartDollyRotate() { + if (scope.enableZoom) handleTouchStartDolly(); + if (scope.enableRotate) handleTouchStartRotate(); + } + function handleTouchMoveRotate(event) { + if (pointers.length == 1) { + rotateEnd.set(event.pageX, event.pageY); + } else { + const position = getSecondPointerPosition(event); + const x = 0.5 * (event.pageX + position.x); + const y = 0.5 * (event.pageY + position.y); + rotateEnd.set(x, y); + } + rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); + const element = scope.domElement; + rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); + rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); + rotateStart.copy(rotateEnd); + } + function handleTouchMovePan(event) { + if (pointers.length === 1) { + panEnd.set(event.pageX, event.pageY); + } else { + const position = getSecondPointerPosition(event); + const x = 0.5 * (event.pageX + position.x); + const y = 0.5 * (event.pageY + position.y); + panEnd.set(x, y); + } + panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); + pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + } + function handleTouchMoveDolly(event) { + const position = getSecondPointerPosition(event); + const dx = event.pageX - position.x; + const dy = event.pageY - position.y; + const distance = Math.sqrt(dx * dx + dy * dy); + dollyEnd.set(0, distance); + dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed)); + dollyOut(dollyDelta.y); + dollyStart.copy(dollyEnd); + } + function handleTouchMoveDollyPan(event) { + if (scope.enableZoom) handleTouchMoveDolly(event); + if (scope.enablePan) handleTouchMovePan(event); + } + function handleTouchMoveDollyRotate(event) { + if (scope.enableZoom) handleTouchMoveDolly(event); + if (scope.enableRotate) handleTouchMoveRotate(event); + } + function onPointerDown(event) { + if (scope.enabled === false) return; + if (pointers.length === 0) { + scope.domElement.setPointerCapture(event.pointerId); + scope.domElement.addEventListener("pointermove", onPointerMove); + scope.domElement.addEventListener("pointerup", onPointerUp); + } + addPointer(event); + if (event.pointerType === "touch") { + onTouchStart(event); + } else { + onMouseDown(event); + } + } + function onPointerMove(event) { + if (scope.enabled === false) return; + if (!is_in_scene(event)) return; + if (event.pointerType === "touch") { + onTouchMove(event); + } else { + onMouseMove(event); + } + } + function onPointerUp(event) { + removePointer(event); + if (pointers.length === 0) { + scope.domElement.releasePointerCapture(event.pointerId); + scope.domElement.removeEventListener("pointermove", onPointerMove); + scope.domElement.removeEventListener("pointerup", onPointerUp); + } + scope.dispatchEvent(_endEvent); + state = STATE.NONE; + } + function onMouseDown(event) { + let mouseAction; + switch(event.button){ + case 0: + mouseAction = scope.mouseButtons.LEFT; + break; + case 1: + mouseAction = scope.mouseButtons.MIDDLE; + break; + case 2: + mouseAction = scope.mouseButtons.RIGHT; + break; + default: + mouseAction = -1; + } + switch(mouseAction){ + case zx.DOLLY: + if (scope.enableZoom === false) return; + handleMouseDownDolly(event); + state = STATE.DOLLY; + break; + case zx.ROTATE: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (scope.enablePan === false) return; + handleMouseDownPan(event); + state = STATE.PAN; + } else { + if (scope.enableRotate === false) return; + handleMouseDownRotate(event); + state = STATE.ROTATE; + } + break; + case zx.PAN: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (scope.enableRotate === false) return; + handleMouseDownRotate(event); + state = STATE.ROTATE; + } else { + if (scope.enablePan === false) return; + handleMouseDownPan(event); + state = STATE.PAN; + } + break; + default: + state = STATE.NONE; + } + if (state !== STATE.NONE) { + scope.dispatchEvent(_startEvent); + } + } + function onMouseMove(event) { + switch(state){ + case STATE.ROTATE: + if (scope.enableRotate === false) return; + handleMouseMoveRotate(event); + break; + case STATE.DOLLY: + if (scope.enableZoom === false) return; + handleMouseMoveDolly(event); + break; + case STATE.PAN: + if (scope.enablePan === false) return; + handleMouseMovePan(event); + break; + } + } + function onMouseWheel(event) { + if (scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE || !is_in_scene(event)) return; + event.preventDefault(); + scope.dispatchEvent(_startEvent); + handleMouseWheel(event); + scope.dispatchEvent(_endEvent); + } + function onKeyDown(event) { + if (scope.enabled === false || scope.enablePan === false) return; + handleKeyDown(event); + } + function onTouchStart(event) { + trackPointer(event); + switch(pointers.length){ + case 1: + switch(scope.touches.ONE){ + case Vx.ROTATE: + if (scope.enableRotate === false) return; + handleTouchStartRotate(); + state = STATE.TOUCH_ROTATE; + break; + case Vx.PAN: + if (scope.enablePan === false) return; + handleTouchStartPan(); + state = STATE.TOUCH_PAN; + break; + default: + state = STATE.NONE; + } + break; + case 2: + switch(scope.touches.TWO){ + case Vx.DOLLY_PAN: + if (scope.enableZoom === false && scope.enablePan === false) return; + handleTouchStartDollyPan(); + state = STATE.TOUCH_DOLLY_PAN; + break; + case Vx.DOLLY_ROTATE: + if (scope.enableZoom === false && scope.enableRotate === false) return; + handleTouchStartDollyRotate(); + state = STATE.TOUCH_DOLLY_ROTATE; + break; + default: + state = STATE.NONE; + } + break; + default: + state = STATE.NONE; + } + if (state !== STATE.NONE) { + scope.dispatchEvent(_startEvent); + } + } + function onTouchMove(event) { + trackPointer(event); + switch(state){ + case STATE.TOUCH_ROTATE: + if (scope.enableRotate === false) return; + handleTouchMoveRotate(event); + scope.update(); + break; + case STATE.TOUCH_PAN: + if (scope.enablePan === false) return; + handleTouchMovePan(event); + scope.update(); + break; + case STATE.TOUCH_DOLLY_PAN: + if (scope.enableZoom === false && scope.enablePan === false) return; + handleTouchMoveDollyPan(event); + scope.update(); + break; + case STATE.TOUCH_DOLLY_ROTATE: + if (scope.enableZoom === false && scope.enableRotate === false) return; + handleTouchMoveDollyRotate(event); + scope.update(); + break; + default: + state = STATE.NONE; + } + } + function onContextMenu(event) { + if (scope.enabled === false) return; + event.preventDefault(); + } + function addPointer(event) { + pointers.push(event); + } + function removePointer(event) { + delete pointerPositions[event.pointerId]; + for(let i = 0; i < pointers.length; i++){ + if (pointers[i].pointerId == event.pointerId) { + pointers.splice(i, 1); + return; + } + } + } + function trackPointer(event) { + let position = pointerPositions[event.pointerId]; + if (position === undefined) { + position = new Z(); + pointerPositions[event.pointerId] = position; + } + position.set(event.pageX, event.pageY); + } + function getSecondPointerPosition(event) { + const pointer = event.pointerId === pointers[0].pointerId ? pointers[1] : pointers[0]; + return pointerPositions[pointer.pointerId]; + } + scope.domElement.addEventListener("contextmenu", onContextMenu); + scope.domElement.addEventListener("pointerdown", onPointerDown); + scope.domElement.addEventListener("pointercancel", onPointerUp); + scope.domElement.addEventListener("wheel", onMouseWheel, { + passive: false + }); + this.update(); + } +} +function events2unitless(screen, event) { + const { canvas , winscale , renderer } = screen; const rect = canvas.getBoundingClientRect(); - const x = (event.clientX - rect.left) * pixelRatio; - const y = (rect.height - (event.clientY - rect.top)) * pixelRatio; + const x = (event.clientX - rect.left) / winscale; + const y = (event.clientY - rect.top) / winscale; return [ x, - y + renderer._height - y ]; } function Identity4x4() { return new ze(); } function in_scene(scene, mouse_event) { - const [x, y] = event2scene_pixel(scene, mouse_event); - const [sx, sy, sw, sh] = scene.pixelarea.value; + const [x, y] = events2unitless(scene.screen, mouse_event); + const [sx, sy, sw, sh] = scene.viewport.value; return x >= sx && x < sx + sw && y >= sy && y < sy + sh; } -function attach_3d_camera(canvas, makie_camera, cam3d, scene) { +function attach_3d_camera(canvas, makie_camera, cam3d, light_dir, scene) { if (cam3d === undefined) { return; } const [w, h] = makie_camera.resolution.value; - const camera = new yt(cam3d.fov, w / h, cam3d.near, cam3d.far); - const center = new A(...cam3d.lookat); - camera.up = new A(...cam3d.upvector); - camera.position.set(...cam3d.eyeposition); + const camera = new yt(cam3d.fov.value, w / h, 0.01, 100.0); + const center = new A(...cam3d.lookat.value); + camera.up = new A(...cam3d.upvector.value); + camera.position.set(...cam3d.eyeposition.value); camera.lookAt(center); - function update() { + const use_orbit_cam = ()=>!(JSServe.can_send_to_julia && JSServe.can_send_to_julia()); + const controls = new OrbitControls(camera, canvas, use_orbit_cam, (e)=>in_scene(scene, e)); + controls.addEventListener("change", (e)=>{ const view = camera.matrixWorldInverse; const projection = camera.projectionMatrix; const [width, height] = cam3d.resolution.value; @@ -20240,82 +21034,8 @@ function attach_3d_camera(canvas, makie_camera, cam3d, scene) { y, z ]); - } - cam3d.resolution.on(update); - function addMouseHandler(domObject, drag, zoomIn, zoomOut) { - let startDragX = null; - let startDragY = null; - function mouseWheelHandler(e) { - e = window.event || e; - if (!in_scene(scene, e)) { - return; - } - const delta = Math.sign(e.deltaY); - if (delta == -1) { - zoomOut(); - } else if (delta == 1) { - zoomIn(); - } - e.preventDefault(); - } - function mouseDownHandler(e) { - if (!in_scene(scene, e)) { - return; - } - startDragX = e.clientX; - startDragY = e.clientY; - e.preventDefault(); - } - function mouseMoveHandler(e) { - if (!in_scene(scene, e)) { - return; - } - if (startDragX === null || startDragY === null) return; - if (drag) drag(e.clientX - startDragX, e.clientY - startDragY); - startDragX = e.clientX; - startDragY = e.clientY; - e.preventDefault(); - } - function mouseUpHandler(e) { - if (!in_scene(scene, e)) { - return; - } - mouseMoveHandler.call(this, e); - startDragX = null; - startDragY = null; - e.preventDefault(); - } - domObject.addEventListener("wheel", mouseWheelHandler); - domObject.addEventListener("mousedown", mouseDownHandler); - domObject.addEventListener("mousemove", mouseMoveHandler); - domObject.addEventListener("mouseup", mouseUpHandler); - } - function drag(deltaX, deltaY) { - const radPerPixel = Math.PI / 450; - const deltaPhi = radPerPixel * deltaX; - const deltaTheta = radPerPixel * deltaY; - const pos = camera.position.sub(center); - const radius = pos.length(); - let theta = Math.acos(pos.z / radius); - let phi = Math.atan2(pos.y, pos.x); - theta = Math.min(Math.max(theta - deltaTheta, 0), Math.PI); - phi -= deltaPhi; - pos.x = radius * Math.sin(theta) * Math.cos(phi); - pos.y = radius * Math.sin(theta) * Math.sin(phi); - pos.z = radius * Math.cos(theta); - camera.position.add(center); - camera.lookAt(center); - update(); - } - function zoomIn() { - camera.position.sub(center).multiplyScalar(0.9).add(center); - update(); - } - function zoomOut() { - camera.position.sub(center).multiplyScalar(1.1).add(center); - update(); - } - addMouseHandler(canvas, drag, zoomIn, zoomOut); + makie_camera.update_light_dir(light_dir.value); + }); } function mul(a, b) { return b.clone().multiply(a); @@ -20396,6 +21116,7 @@ class MakieCamera { this.resolution = new Pu(new Z()); this.eyeposition = new Pu(new A()); this.preprojections = {}; + this.light_direction = new Pu(new A(-1, -1, -1).normalize()); } calculate_matrices() { const [w, h] = this.resolution.value; @@ -20418,6 +21139,12 @@ class MakieCamera { this.calculate_matrices(); return; } + update_light_dir(light_dir) { + const T = new He().setFromMatrix4(this.view.value).invert(); + const new_dir = new A().fromArray(light_dir); + new_dir.applyMatrix3(T).normalize(); + this.light_direction.value = new_dir; + } clip_to_space(space) { if (space === "data") { return this.projectionview_inverse.value; @@ -20466,6 +21193,18 @@ class MakieCamera { } } const scene_cache = {}; +function filter_by_key(dict, keys, default_value = false) { + const result = {}; + keys.forEach((key)=>{ + const val = dict[key]; + if (val) { + result[key] = val; + } else { + result[key] = default_value; + } + }); + return result; +} const plot_cache = {}; const TEXTURE_ATLAS = [ undefined @@ -20481,6 +21220,7 @@ function delete_scene(scene_id) { if (!scene) { return; } + delete_three_scene(scene); while(scene.children.length > 0){ scene.remove(scene.children[0]); } @@ -20498,7 +21238,10 @@ function find_plots(plot_uuids) { } function delete_scenes(scene_uuids, plot_uuids) { plot_uuids.forEach((plot_id)=>{ - delete plot_cache[plot_id]; + const plot = plot_cache[plot_id]; + if (plot) { + delete_plot(plot); + } }); scene_uuids.forEach((scene_id)=>{ delete_scene(scene_id); @@ -20510,14 +21253,9 @@ function insert_plot(scene_id, plot_data) { add_plot(scene, plot); }); } -function delete_plots(scene_id, plot_uuids) { - console.log(`deleting plots!: ${plot_uuids}`); - const scene = find_scene(scene_id); +function delete_plots(plot_uuids) { const plots = find_plots(plot_uuids); - plots.forEach((p)=>{ - scene.remove(p); - delete plot_cache[p]; - }); + plots.forEach(delete_plot); } function convert_texture(data) { const tex = create_texture(data); @@ -20578,9 +21316,236 @@ function deserialize_uniforms(data) { } return result; } +function linesegments_vertex_shader(uniforms, attributes) { + const attribute_decl = attributes_to_type_declaration(attributes); + const uniform_decl = uniforms_to_type_declaration(uniforms); + const color = attribute_type(attributes.color_start) || uniform_type(uniforms.color_start); + return `precision mediump int; + precision highp float; + + ${attribute_decl} + ${uniform_decl} + + out vec2 f_uv; + out ${color} f_color; + + vec2 get_resolution() { + // 2 * px_per_unit doesn't make any sense, but works + // TODO, figure out what's going on! + return resolution / 2.0 * px_per_unit; + } + + vec3 screen_space(vec3 point) { + vec4 vertex = projectionview * model * vec4(point, 1); + return vec3(vertex.xy * get_resolution(), vertex.z + vertex.w * depth_shift) / vertex.w; + } + + vec3 screen_space(vec2 point) { + return screen_space(vec3(point, 0)); + } + + void main() { + vec3 p_a = screen_space(linepoint_start); + vec3 p_b = screen_space(linepoint_end); + float width = (px_per_unit * (position.x == 1.0 ? linewidth_end : linewidth_start)); + f_color = position.x == 1.0 ? color_end : color_start; + f_uv = vec2(position.x, position.y + 0.5); + + vec2 pointA = p_a.xy; + vec2 pointB = p_b.xy; + + vec2 xBasis = pointB - pointA; + vec2 yBasis = normalize(vec2(-xBasis.y, xBasis.x)); + vec2 point = pointA + xBasis * position.x + yBasis * width * position.y; + + gl_Position = vec4(point.xy / get_resolution(), position.x == 1.0 ? p_b.z : p_a.z, 1.0); + } + `; +} +function lines_fragment_shader(uniforms, attributes) { + const color = attribute_type(attributes.color_start) || uniform_type(uniforms.color_start); + const color_uniforms = filter_by_key(uniforms, [ + "colorrange", + "colormap", + "nan_color", + "highclip", + "lowclip" + ]); + const uniform_decl = uniforms_to_type_declaration(color_uniforms); + return `#extension GL_OES_standard_derivatives : enable + + precision mediump int; + precision highp float; + precision mediump sampler2D; + precision mediump sampler3D; + + in vec2 f_uv; + in ${color} f_color; + ${uniform_decl} + + out vec4 fragment_color; + + // Half width of antialiasing smoothstep + #define ANTIALIAS_RADIUS 0.7071067811865476 + + vec4 get_color_from_cmap(float value, sampler2D colormap, vec2 colorrange) { + float cmin = colorrange.x; + float cmax = colorrange.y; + if (value <= cmax && value >= cmin) { + // in value range, continue! + } else if (value < cmin) { + return lowclip; + } else if (value > cmax) { + return highclip; + } else { + // isnan CAN be broken (of course) -.- + // so if outside value range and not smaller/bigger min/max we assume NaN + return nan_color; + } + float i01 = clamp((value - cmin) / (cmax - cmin), 0.0, 1.0); + // 1/0 corresponds to the corner of the colormap, so to properly interpolate + // between the colors, we need to scale it, so that the ends are at 1 - (stepsize/2) and 0+(stepsize/2). + float stepsize = 1.0 / float(textureSize(colormap, 0)); + i01 = (1.0 - stepsize) * i01 + 0.5 * stepsize; + return texture(colormap, vec2(i01, 0.0)); + } + + vec4 get_color(float color, sampler2D colormap, vec2 colorrange) { + return get_color_from_cmap(color, colormap, colorrange); + } + + vec4 get_color(vec4 color, bool colormap, bool colorrange) { + return color; + } + vec4 get_color(vec3 color, bool colormap, bool colorrange) { + return vec4(color, 1.0); + } + + float aastep(float threshold, float value) { + float afwidth = length(vec2(dFdx(value), dFdy(value))) * ANTIALIAS_RADIUS; + return smoothstep(threshold-afwidth, threshold+afwidth, value); + } + + float aastep(float threshold1, float threshold2, float dist) { + return aastep(threshold1, dist) * aastep(threshold2, 1.0 - dist); + } + + void main(){ + float xalpha = aastep(0.0, 0.0, f_uv.x); + float yalpha = aastep(0.0, 0.0, f_uv.y); + vec4 color = get_color(f_color, colormap, colorrange); + fragment_color = vec4(color.rgb, color.a); + } + `; +} +function create_line_material(uniforms, attributes) { + const uniforms_des = deserialize_uniforms(uniforms); + return new THREE.RawShaderMaterial({ + uniforms: uniforms_des, + glslVersion: THREE.GLSL3, + vertexShader: linesegments_vertex_shader(uniforms_des, attributes), + fragmentShader: lines_fragment_shader(uniforms_des, attributes), + transparent: true + }); +} +function attach_interleaved_line_buffer(attr_name, geometry, points, ndim, is_segments) { + const skip_elems = is_segments ? 2 * ndim : ndim; + const buffer = new THREE.InstancedInterleavedBuffer(points, skip_elems, 1); + geometry.setAttribute(attr_name + "_start", new THREE.InterleavedBufferAttribute(buffer, ndim, 0)); + geometry.setAttribute(attr_name + "_end", new THREE.InterleavedBufferAttribute(buffer, ndim, ndim)); + return buffer; +} +function create_line_instance_geometry() { + const geometry = new THREE.InstancedBufferGeometry(); + const instance_positions = [ + 0, + -0.5, + 1, + -0.5, + 1, + 0.5, + 0, + -0.5, + 1, + 0.5, + 0, + 0.5 + ]; + geometry.setAttribute("position", new THREE.Float32BufferAttribute(instance_positions, 2)); + geometry.boundingSphere = new THREE.Sphere(); + geometry.boundingSphere.radius = 10000000000000; + geometry.frustumCulled = false; + return geometry; +} +function create_line_buffer(geometry, buffers, name, attr, is_segments) { + const flat_buffer = attr.value.flat; + const ndims = attr.value.type_length; + const linebuffer = attach_interleaved_line_buffer(name, geometry, flat_buffer, ndims, is_segments); + buffers[name] = linebuffer; + return flat_buffer; +} +function create_line_buffers(geometry, buffers, attributes, is_segments) { + for(let name in attributes){ + const attr = attributes[name]; + create_line_buffer(geometry, buffers, name, attr, is_segments); + } +} +function attach_updates(mesh, buffers, attributes, is_segments) { + let geometry = mesh.geometry; + for(let name in attributes){ + const attr = attributes[name]; + attr.on((new_points)=>{ + let buff = buffers[name]; + const ndims = new_points.type_length; + const new_line_points = new_points.flat; + const old_count = buff.array.length; + const new_count = new_line_points.length / ndims; + if (old_count < new_line_points.length) { + mesh.geometry.dispose(); + geometry = create_line_instance_geometry(); + buff = attach_interleaved_line_buffer(name, geometry, new_line_points, ndims, is_segments); + mesh.geometry = geometry; + buffers[name] = buff; + } else { + buff.set(new_line_points); + } + const ls_factor = is_segments ? 2 : 1; + const offset = is_segments ? 0 : 1; + mesh.geometry.instanceCount = Math.max(0, new_count / ls_factor - offset); + buff.needsUpdate = true; + mesh.needsUpdate = true; + }); + } +} +function _create_line(line_data, is_segments) { + const geometry = create_line_instance_geometry(); + const buffers = {}; + create_line_buffers(geometry, buffers, line_data.attributes, is_segments); + const material = create_line_material(line_data.uniforms, geometry.attributes); + const mesh = new THREE.Mesh(geometry, material); + const offset = is_segments ? 0 : 1; + const new_count = geometry.attributes.linepoint_start.count; + mesh.geometry.instanceCount = Math.max(0, new_count - offset); + attach_updates(mesh, buffers, line_data.attributes, is_segments); + return mesh; +} +function create_line(line_data) { + return _create_line(line_data, false); +} +function create_linesegments(line_data) { + return _create_line(line_data, true); +} function deserialize_plot(data) { let mesh; - if ("instance_attributes" in data) { + const update_visible = (v)=>{ + mesh.visible = v; + return; + }; + if (data.plot_type === "lines") { + mesh = create_line(data); + } else if (data.plot_type === "linesegments") { + mesh = create_linesegments(data); + } else if ("instance_attributes" in data) { mesh = create_instanced_mesh(data); } else { mesh = create_mesh(data); @@ -20589,14 +21554,12 @@ function deserialize_plot(data) { mesh.frustumCulled = false; mesh.matrixAutoUpdate = false; mesh.plot_uuid = data.uuid; - const update_visible = (v)=>{ - mesh.visible = v; - return; - }; update_visible(data.visible.value); data.visible.on(update_visible); connect_uniforms(mesh, data.uniform_updater); - connect_attributes(mesh, data.attribute_updater); + if (!(data.plot_type === "lines" || data.plot_type === "linesegments")) { + connect_attributes(mesh, data.attribute_updater); + } return mesh; } const ON_NEXT_INSERT = new Set(); @@ -20624,13 +21587,27 @@ function add_plot(scene, plot_data) { plot_data.uniforms.projection = identity; plot_data.uniforms.projectionview = identity; } + const { px_per_unit } = scene.screen; plot_data.uniforms.resolution = cam.resolution; + plot_data.uniforms.px_per_unit = new mod.Uniform(px_per_unit); if (plot_data.uniforms.preprojection) { const { space , markerspace } = plot_data; plot_data.uniforms.preprojection = cam.preprojection_matrix(space.value, markerspace.value); } + if (scene.camera_relative_light) { + plot_data.uniforms.light_direction = cam.light_direction; + scene.light_direction.on((value)=>{ + cam.update_light_dir(value); + }); + } else { + const light_dir = new mod.Vector3().fromArray(scene.light_direction.value); + plot_data.uniforms.light_direction = new mod.Uniform(light_dir); + scene.light_direction.on((value)=>{ + plot_data.uniforms.light_direction.value.fromArray(value); + }); + } const p = deserialize_plot(plot_data); - plot_cache[plot_data.uuid] = p; + plot_cache[p.plot_uuid] = p; scene.add(p); const next_insert = new Set(ON_NEXT_INSERT); next_insert.forEach((f)=>f()); @@ -20895,26 +21872,37 @@ function deserialize_scene(data, screen) { add_scene(data.uuid, scene); scene.scene_uuid = data.uuid; scene.frustumCulled = false; - scene.pixelarea = data.pixelarea; + scene.viewport = data.viewport; scene.backgroundcolor = data.backgroundcolor; + scene.backgroundcolor_alpha = data.backgroundcolor_alpha; scene.clearscene = data.clearscene; scene.visible = data.visible; + scene.camera_relative_light = data.camera_relative_light; + scene.light_direction = data.light_direction; const camera = new MakieCamera(); scene.wgl_camera = camera; - function update_cam(camera_matrices) { + function update_cam(camera_matrices, force) { + if (!force) { + if (!(JSServe.can_send_to_julia && JSServe.can_send_to_julia())) { + return; + } + } const [view, projection, resolution, eyepos] = camera_matrices; camera.update_matrices(view, projection, resolution, eyepos); } - update_cam(data.camera.value); if (data.cam3d_state) { - attach_3d_camera(canvas, camera, data.cam3d_state, scene); - } else { - data.camera.on(update_cam); + attach_3d_camera(canvas, camera, data.cam3d_state, data.light_direction, scene); } + update_cam(data.camera.value, true); + camera.update_light_dir(data.light_direction.value); + data.camera.on(update_cam); data.plots.forEach((plot_data)=>{ add_plot(scene, plot_data); }); - scene.scene_children = data.children.map((child)=>deserialize_scene(child, screen)); + scene.scene_children = data.children.map((child)=>{ + const childscene = deserialize_scene(child, screen); + return childscene; + }); return scene; } function delete_plot(plot) { @@ -20934,24 +21922,23 @@ function delete_three_scene(scene) { } } window.THREE = mod; -const pixelRatio1 = window.devicePixelRatio || 1.0; function render_scene(scene, picking = false) { - const { camera , renderer } = scene.screen; + const { camera , renderer , px_per_unit } = scene.screen; const canvas = renderer.domElement; if (!document.body.contains(canvas)) { console.log("EXITING WGL"); + delete_three_scene(scene); renderer.state.reset(); renderer.dispose(); - delete_three_scene(scene); return false; } if (!scene.visible.value) { return true; } renderer.autoClear = scene.clearscene.value; - const area = scene.pixelarea.value; + const area = scene.viewport.value; if (area) { - const [x, y, w, h] = area.map((t)=>t / pixelRatio1); + const [x, y, w, h] = area.map((x)=>x * px_per_unit); renderer.setViewport(x, y, w, h); renderer.setScissor(x, y, w, h); renderer.setScissorTest(true); @@ -20959,7 +21946,8 @@ function render_scene(scene, picking = false) { renderer.setClearAlpha(0); renderer.setClearColor(new mod.Color(0), 0.0); } else { - renderer.setClearColor(scene.backgroundcolor.value); + const alpha = scene.backgroundcolor_alpha.value; + renderer.setClearColor(scene.backgroundcolor.value, alpha); } renderer.render(scene, camera); } @@ -21000,6 +21988,24 @@ function throttle_function(func, delay) { } return inner_throttle; } +function get_body_size() { + const bodyStyle = window.getComputedStyle(document.body); + const width_padding = parseInt(bodyStyle.paddingLeft, 10) + parseInt(bodyStyle.paddingRight, 10) + parseInt(bodyStyle.marginLeft, 10) + parseInt(bodyStyle.marginRight, 10); + const height_padding = parseInt(bodyStyle.paddingTop, 10) + parseInt(bodyStyle.paddingBottom, 10) + parseInt(bodyStyle.marginTop, 10) + parseInt(bodyStyle.marginBottom, 10); + const width = window.innerWidth - width_padding; + const height = window.innerHeight - height_padding; + return [ + width, + height + ]; +} +function get_parent_size(canvas) { + const rect = canvas.parentElement.getBoundingClientRect(); + return [ + rect.width, + rect.height + ]; +} function wglerror(gl, error) { switch(error){ case gl.NO_ERROR: @@ -21050,41 +22056,20 @@ function on_shader_error(gl, program, glVertexShader, glFragmentShader) { const err = "THREE.WebGLProgram: Shader Error " + wglerror(gl, gl.getError()) + " - " + "VALIDATE_STATUS " + gl.getProgramParameter(program, gl.VALIDATE_STATUS) + "\n\n" + "Program Info Log:\n" + programLog + "\n" + vertexErrors + "\n" + fragmentErrors + "\n" + "Fragment log:\n" + fragmentLog + "Vertex log:\n" + vertexLog; JSServe.Connection.send_warning(err); } -function threejs_module(canvas, comm, width, height, resize_to_body) { - let context = canvas.getContext("webgl2", { - preserveDrawingBuffer: true - }); - if (!context) { - console.warn("WebGL 2.0 not supported by browser, falling back to WebGL 1.0 (Volume plots will not work)"); - context = canvas.getContext("webgl", { - preserveDrawingBuffer: true - }); - } - if (!context) { - return; - } - const renderer = new mod.WebGLRenderer({ - antialias: true, - canvas: canvas, - context: context, - powerPreference: "high-performance" - }); - renderer.debug.onShaderError = on_shader_error; - renderer.setClearColor("#ffffff"); - renderer.setPixelRatio(pixelRatio1); - renderer.setSize(width / pixelRatio1, height / pixelRatio1); - const mouse_callback = (x, y)=>comm.notify({ +function add_canvas_events(screen, comm, resize_to) { + const { canvas , winscale } = screen; + function mouse_callback(event) { + const [x, y] = events2unitless(screen, event); + comm.notify({ mouseposition: [ x, y ] }); + } const notify_mouse_throttled = throttle_function(mouse_callback, 40); function mousemove(event) { - var rect = canvas.getBoundingClientRect(); - var x = (event.clientX - rect.left) * pixelRatio1; - var y = (event.clientY - rect.top) * pixelRatio1; - notify_mouse_throttled(x, y); + notify_mouse_throttled(event); return false; } canvas.addEventListener("mousemove", mousemove); @@ -21136,52 +22121,115 @@ function threejs_module(canvas, comm, width, height, resize_to_body) { canvas.addEventListener("contextmenu", (e)=>e.preventDefault()); canvas.addEventListener("focusout", contextmenu); function resize_callback() { - const bodyStyle = window.getComputedStyle(document.body); - const width_padding = parseInt(bodyStyle.paddingLeft, 10) + parseInt(bodyStyle.paddingRight, 10) + parseInt(bodyStyle.marginLeft, 10) + parseInt(bodyStyle.marginRight, 10); - const height_padding = parseInt(bodyStyle.paddingTop, 10) + parseInt(bodyStyle.paddingBottom, 10) + parseInt(bodyStyle.marginTop, 10) + parseInt(bodyStyle.marginBottom, 10); - const width = (window.innerWidth - width_padding) * pixelRatio1; - const height = (window.innerHeight - height_padding) * pixelRatio1; + let width, height; + if (resize_to == "body") { + [width, height] = get_body_size(); + } else if (resize_to == "parent") { + [width, height] = get_parent_size(canvas); + } comm.notify({ resize: [ - width, - height + width / winscale, + height / winscale ] }); } - if (resize_to_body) { + if (resize_to) { const resize_callback_throttled = throttle_function(resize_callback, 100); window.addEventListener("resize", (event)=>resize_callback_throttled()); resize_callback_throttled(); } +} +function threejs_module(canvas) { + let context = canvas.getContext("webgl2", { + preserveDrawingBuffer: true + }); + if (!context) { + console.warn("WebGL 2.0 not supported by browser, falling back to WebGL 1.0 (Volume plots will not work)"); + context = canvas.getContext("webgl", { + preserveDrawingBuffer: true + }); + } + if (!context) { + return; + } + const renderer = new mod.WebGLRenderer({ + antialias: true, + canvas: canvas, + context: context, + powerPreference: "high-performance" + }); + renderer.debug.onShaderError = on_shader_error; + renderer.setClearColor("#ffffff"); return renderer; } -function create_scene(wrapper, canvas, canvas_width, scenes, comm, width, height, texture_atlas_obs, fps, resize_to_body) { - const renderer = threejs_module(canvas, comm, width, height, resize_to_body); +function set_render_size(screen, width, height) { + const { renderer , canvas , scalefactor , winscale , px_per_unit } = screen; + const [swidth, sheight] = [ + winscale * width, + winscale * height + ]; + const real_pixel_width = Math.ceil(width * px_per_unit); + const real_pixel_height = Math.ceil(height * px_per_unit); + renderer._width = width; + renderer._height = height; + canvas.width = real_pixel_width; + canvas.height = real_pixel_height; + canvas.style.width = swidth + "px"; + canvas.style.height = sheight + "px"; + renderer.setViewport(0, 0, real_pixel_width, real_pixel_height); + add_picking_target(screen); + return; +} +function add_picking_target(screen) { + const { picking_target , canvas } = screen; + const [w, h] = [ + canvas.width, + canvas.height + ]; + if (picking_target) { + if (picking_target.width == w && picking_target.height == h) { + return; + } else { + picking_target.dispose(); + } + } + screen.picking_target = new mod.WebGLRenderTarget(w, h); + return; +} +function create_scene(wrapper, canvas, canvas_width, scenes, comm, width, height, texture_atlas_obs, fps, resize_to, px_per_unit, scalefactor) { + if (!scalefactor) { + scalefactor = window.devicePixelRatio || 1.0; + } + if (!px_per_unit) { + px_per_unit = scalefactor; + } + const renderer = threejs_module(canvas); TEXTURE_ATLAS[0] = texture_atlas_obs; - if (renderer) { - const camera = new mod.PerspectiveCamera(45, 1, 0, 100); - camera.updateProjectionMatrix(); - const size = new mod.Vector2(); - renderer.getDrawingBufferSize(size); - const picking_target = new mod.WebGLRenderTarget(size.x, size.y); - const screen = { - renderer, - picking_target, - camera, - fps, - canvas - }; - const three_scene = deserialize_scene(scenes, screen); - console.log(three_scene); - start_renderloop(three_scene); - canvas_width.on((w_h)=>{ - const pixelRatio = renderer.getPixelRatio(); - renderer.setSize(w_h[0] / pixelRatio, w_h[1] / pixelRatio); - }); - } else { + if (!renderer) { const warning = getWebGLErrorMessage(); wrapper.appendChild(warning); } + const camera = new mod.PerspectiveCamera(45, 1, 0, 100); + camera.updateProjectionMatrix(); + const pixel_ratio = window.devicePixelRatio || 1.0; + const winscale = scalefactor / pixel_ratio; + const screen = { + renderer, + camera, + fps, + canvas, + px_per_unit, + scalefactor, + winscale + }; + add_canvas_events(screen, comm, resize_to); + set_render_size(screen, width, height); + const three_scene = deserialize_scene(scenes, screen); + start_renderloop(three_scene); + canvas_width.on((w_h)=>{ + set_render_size(screen, ...w_h); + }); return renderer; } function set_picking_uniforms(scene, last_id, picking, picked_plots, plots, id_to_plot) { @@ -21211,8 +22259,20 @@ function set_picking_uniforms(scene, last_id, picking, picked_plots, plots, id_t }); return next_id; } -function pick_native(scene, x, y, w, h) { - const { renderer , picking_target } = scene.screen; +function pick_native(scene, _x, _y, _w, _h) { + const { renderer , picking_target , px_per_unit } = scene.screen; + [_x, _y, _w, _h] = [ + _x, + _y, + _w, + _h + ].map((x)=>Math.ceil(x * px_per_unit)); + const [x, y, w, h] = [ + _x, + _y, + _w, + _h + ]; renderer.setRenderTarget(picking_target); set_picking_uniforms(scene, 1, true); render_scene(scene, true); @@ -21255,8 +22315,11 @@ function pick_native(scene, x, y, w, h) { ]; } function pick_closest(scene, xy, range) { - const { picking_target } = scene.screen; - const { width , height } = picking_target; + const { renderer } = scene.screen; + const [width, height] = [ + renderer._width, + renderer._height + ]; if (!(1.0 <= xy[0] <= width && 1.0 <= xy[1] <= height)) { return [ null, @@ -21296,8 +22359,11 @@ function pick_closest(scene, xy, range) { return selection; } function pick_sorted(scene, xy, range) { - const { picking_target } = scene.screen; - const { width , height } = picking_target; + const { renderer } = scene.screen; + const [width, height] = [ + renderer._width, + renderer._height + ]; if (!(1.0 <= xy[0] <= width && 1.0 <= xy[1] <= height)) { return null; } @@ -21319,6 +22385,9 @@ function pick_sorted(scene, xy, range) { for(let i = 1; i <= dx; i++){ for(let j = 1; j <= dx; j++){ const d = x - i ^ 2 + (y - j) ^ 2; + if (plot_matrix.length <= pindex) { + continue; + } const [plot_uuid, index] = plot_matrix[pindex]; pindex = pindex + 1; const plot_index = selected.findIndex((x)=>x[0].plot_uuid == plot_uuid); @@ -21353,17 +22422,17 @@ function register_popup(popup, scene, plots_to_pick, callback) { } const { canvas } = scene.screen; canvas.addEventListener("mousedown", (event)=>{ - if (!popup.classList.contains("show")) { - popup.classList.add("show"); - } - popup.style.left = event.pageX + "px"; - popup.style.top = event.pageY + "px"; - const [x, y] = event2scene_pixel(scene, event); + const [x, y] = events2unitless(scene.screen, event); const [_, picks] = pick_native(scene, x, y, 1, 1); if (picks.length == 1) { const [plot, index] = picks[0]; if (plots_to_pick.has(plot.plot_uuid)) { const result = callback(plot, index); + if (!popup.classList.contains("show")) { + popup.classList.add("show"); + } + popup.style.left = event.pageX + "px"; + popup.style.top = event.pageY + "px"; if (typeof result === "string" || result instanceof String) { popup.innerText = result; } else { @@ -21393,12 +22462,12 @@ window.WGL = { plot_cache, delete_scenes, create_scene, - event2scene_pixel, + events2unitless, on_next_insert, register_popup, render_scene }; -export { deserialize_scene as deserialize_scene, threejs_module as threejs_module, start_renderloop as start_renderloop, delete_plots as delete_plots, insert_plot as insert_plot, find_plots as find_plots, delete_scene as delete_scene, find_scene as find_scene, scene_cache as scene_cache, plot_cache as plot_cache, delete_scenes as delete_scenes, create_scene as create_scene, event2scene_pixel as event2scene_pixel, on_next_insert as on_next_insert }; +export { deserialize_scene as deserialize_scene, threejs_module as threejs_module, start_renderloop as start_renderloop, delete_plots as delete_plots, insert_plot as insert_plot, find_plots as find_plots, delete_scene as delete_scene, find_scene as find_scene, scene_cache as scene_cache, plot_cache as plot_cache, delete_scenes as delete_scenes, create_scene as create_scene, events2unitless as events2unitless, on_next_insert as on_next_insert }; export { render_scene as render_scene }; export { wglerror as wglerror }; export { pick_native as pick_native }; diff --git a/WGLMakie/src/wglmakie.js b/WGLMakie/src/wglmakie.js index 66bdebe48bb..a9986046845 100644 --- a/WGLMakie/src/wglmakie.js +++ b/WGLMakie/src/wglmakie.js @@ -1,4 +1,4 @@ -import * as THREE from "https://cdn.esm.sh/v66/three@0.157/es2021/three.js"; +import * as THREE from "./THREE.js"; import { getWebGLErrorMessage } from "./WEBGL.js"; import { delete_scenes, @@ -15,20 +15,18 @@ import { find_scene, } from "./Serialization.js"; -import { event2scene_pixel } from "./Camera.js"; +import { events2unitless } from "./Camera.js"; window.THREE = THREE; -const pixelRatio = window.devicePixelRatio || 1.0; - export function render_scene(scene, picking = false) { - const { camera, renderer } = scene.screen; + const { camera, renderer, px_per_unit } = scene.screen; const canvas = renderer.domElement; if (!document.body.contains(canvas)) { console.log("EXITING WGL"); + delete_three_scene(scene); renderer.state.reset(); renderer.dispose(); - delete_three_scene(scene); return false; } // dont render invisible scenes @@ -36,9 +34,9 @@ export function render_scene(scene, picking = false) { return true; } renderer.autoClear = scene.clearscene.value; - const area = scene.pixelarea.value; + const area = scene.viewport.value; if (area) { - const [x, y, w, h] = area.map((t) => t / pixelRatio); + const [x, y, w, h] = area.map((x) => x * px_per_unit); renderer.setViewport(x, y, w, h); renderer.setScissor(x, y, w, h); renderer.setScissorTest(true); @@ -46,7 +44,8 @@ export function render_scene(scene, picking = false) { renderer.setClearAlpha(0); renderer.setClearColor(new THREE.Color(0), 0.0); } else { - renderer.setClearColor(scene.backgroundcolor.value); + const alpha = scene.backgroundcolor_alpha.value; + renderer.setClearColor(scene.backgroundcolor.value, alpha); } renderer.render(scene, camera); } @@ -115,6 +114,28 @@ function throttle_function(func, delay) { return inner_throttle; } +function get_body_size() { + const bodyStyle = window.getComputedStyle(document.body); + // Subtract padding that is added by VSCode + const width_padding = + parseInt(bodyStyle.paddingLeft, 10) + + parseInt(bodyStyle.paddingRight, 10) + + parseInt(bodyStyle.marginLeft, 10) + + parseInt(bodyStyle.marginRight, 10); + const height_padding = + parseInt(bodyStyle.paddingTop, 10) + + parseInt(bodyStyle.paddingBottom, 10) + + parseInt(bodyStyle.marginTop, 10) + + parseInt(bodyStyle.marginBottom, 10); + const width = (window.innerWidth - width_padding); + const height = (window.innerHeight - height_padding); + return [width, height]; +} +function get_parent_size(canvas) { + const rect = canvas.parentElement.getBoundingClientRect(); + return [rect.width, rect.height]; +} + export function wglerror(gl, error) { switch (error) { case gl.NO_ERROR: @@ -202,48 +223,17 @@ function on_shader_error(gl, program, glVertexShader, glFragmentShader) { JSServe.Connection.send_warning(err); } -function threejs_module(canvas, comm, width, height, resize_to_body) { - let context = canvas.getContext("webgl2", { - preserveDrawingBuffer: true, - }); - if (!context) { - console.warn( - "WebGL 2.0 not supported by browser, falling back to WebGL 1.0 (Volume plots will not work)" - ); - context = canvas.getContext("webgl", { - preserveDrawingBuffer: true, - }); - } - if (!context) { - // Sigh, safari or something - // we return nothing which will be handled by caller - return; +function add_canvas_events(screen, comm, resize_to) { + const { canvas, winscale } = screen; + function mouse_callback(event) { + const [x, y] = events2unitless(screen, event); + comm.notify({ mouseposition: [x, y] }); } - const renderer = new THREE.WebGLRenderer({ - antialias: true, - canvas: canvas, - context: context, - powerPreference: "high-performance", - }); - - renderer.debug.onShaderError = on_shader_error; - - renderer.setClearColor("#ffffff"); - - // The following handles high-DPI devices - // `renderer.setSize` also updates `canvas` size - renderer.setPixelRatio(pixelRatio); - renderer.setSize(width / pixelRatio, height / pixelRatio); - const mouse_callback = (x, y) => comm.notify({ mouseposition: [x, y] }); const notify_mouse_throttled = throttle_function(mouse_callback, 40); function mousemove(event) { - var rect = canvas.getBoundingClientRect(); - var x = (event.clientX - rect.left) * pixelRatio; - var y = (event.clientY - rect.top) * pixelRatio; - - notify_mouse_throttled(x, y); + notify_mouse_throttled(event); return false; } @@ -309,25 +299,16 @@ function threejs_module(canvas, comm, width, height, resize_to_body) { canvas.addEventListener("focusout", contextmenu); function resize_callback() { - const bodyStyle = window.getComputedStyle(document.body); - // Subtract padding that is added by VSCode - const width_padding = - parseInt(bodyStyle.paddingLeft, 10) + - parseInt(bodyStyle.paddingRight, 10) + - parseInt(bodyStyle.marginLeft, 10) + - parseInt(bodyStyle.marginRight, 10); - const height_padding = - parseInt(bodyStyle.paddingTop, 10) + - parseInt(bodyStyle.paddingBottom, 10) + - parseInt(bodyStyle.marginTop, 10) + - parseInt(bodyStyle.marginBottom, 10); - const width = (window.innerWidth - width_padding) * pixelRatio; - const height = (window.innerHeight - height_padding) * pixelRatio; - + let width, height; + if (resize_to == "body") { + [width, height] = get_body_size(); + } else if (resize_to == "parent") { + [width, height] = get_parent_size(canvas); + } // Send the resize event to Julia - comm.notify({ resize: [width, height] }); + comm.notify({ resize: [width / winscale, height / winscale] }); } - if (resize_to_body) { + if (resize_to) { const resize_callback_throttled = throttle_function( resize_callback, 100 @@ -338,10 +319,86 @@ function threejs_module(canvas, comm, width, height, resize_to_body) { // Fire the resize event once at the start to auto-size our window resize_callback_throttled(); } +} + +function threejs_module(canvas) { + + let context = canvas.getContext("webgl2", { + preserveDrawingBuffer: true, + }); + if (!context) { + console.warn( + "WebGL 2.0 not supported by browser, falling back to WebGL 1.0 (Volume plots will not work)" + ); + context = canvas.getContext("webgl", { + preserveDrawingBuffer: true, + }); + } + if (!context) { + // Sigh, safari or something + // we return nothing which will be handled by caller + return; + } + + const renderer = new THREE.WebGLRenderer({ + antialias: true, + canvas: canvas, + context: context, + powerPreference: "high-performance", + }); + + renderer.debug.onShaderError = on_shader_error; + renderer.setClearColor("#ffffff"); return renderer; } +function set_render_size(screen, width, height) { + const { renderer, canvas, scalefactor, winscale, px_per_unit } = screen; + // The displayed size of the canvas, in CSS pixels - which get scaled by the device pixel ratio + const [swidth, sheight] = [winscale * width, winscale * height]; + + const real_pixel_width = Math.ceil(width * px_per_unit); + const real_pixel_height = Math.ceil(height * px_per_unit); + + renderer._width = width; + renderer._height = height; + + canvas.width = real_pixel_width; + canvas.height = real_pixel_height; + + canvas.style.width = swidth + "px"; + canvas.style.height = sheight + "px"; + + renderer.setViewport(0, 0, real_pixel_width, real_pixel_height); + add_picking_target(screen); + return; +} + +function add_picking_target(screen) { + const { picking_target, canvas } = screen; + const [w, h] = [canvas.width, canvas.height]; + if (picking_target) { + if (picking_target.width == w && picking_target.height == h) { + return + } else { + picking_target.dispose(); + } + } + // BIG TODO here... + // We should only make the picking target as big as the area we're picking + // e.g. for just the mouse position it should be 1x1 + // Or we should just always bind the target and render to it in one pass + // 1) One Pass: + // Only works on WebGL 2.0, which is still not as widely supported + // Also it's a bit more complicated to setup + // 2) Only Area we pick + // It's currently not as easy to change the offset + area of the camera + // So, we'll need to make that easier first + screen.picking_target = new THREE.WebGLRenderTarget(w, h); + return; +} + function create_scene( wrapper, canvas, @@ -352,49 +409,51 @@ function create_scene( height, texture_atlas_obs, fps, - resize_to_body + resize_to, + px_per_unit, + scalefactor ) { - const renderer = threejs_module( - canvas, - comm, - width, - height, - resize_to_body - ); + if (!scalefactor) { + scalefactor = window.devicePixelRatio || 1.0; + } + if (!px_per_unit) { + px_per_unit = scalefactor; + } + + const renderer = threejs_module(canvas); + TEXTURE_ATLAS[0] = texture_atlas_obs; - if (renderer) { - const camera = new THREE.PerspectiveCamera(45, 1, 0, 100); - camera.updateProjectionMatrix(); - const size = new THREE.Vector2(); - renderer.getDrawingBufferSize(size); - // BIG TODO here... - // We should only make the picking target as big as the area we're picking - // e.g. for just the mouse position it should be 1x1 - // Or we should just always bind the target and render to it in one pass - // 1) One Pass: - // Only works on WebGL 2.0, which is still not as widely supported - // Also it's a bit more complicated to setup - // 2) Only Area we pick - // It's currently not as easy to change the offset + area of the camera - // So, we'll need to make that easier first - const picking_target = new THREE.WebGLRenderTarget(size.x, size.y); - const screen = { renderer, picking_target, camera, fps, canvas }; - - const three_scene = deserialize_scene(scenes, screen); - console.log(three_scene); - start_renderloop(three_scene); - - canvas_width.on((w_h) => { - // `renderer.setSize` correctly updates `canvas` dimensions - const pixelRatio = renderer.getPixelRatio(); - renderer.setSize(w_h[0] / pixelRatio, w_h[1] / pixelRatio); - }); - } else { + if (!renderer) { const warning = getWebGLErrorMessage(); // wrapper.removeChild(canvas) wrapper.appendChild(warning); } + + const camera = new THREE.PerspectiveCamera(45, 1, 0, 100); + camera.updateProjectionMatrix(); + const pixel_ratio = window.devicePixelRatio || 1.0; + const winscale = scalefactor / pixel_ratio; + const screen = { + renderer, + camera, + fps, + canvas, + px_per_unit, + scalefactor, + winscale, + }; + add_canvas_events(screen, comm, resize_to); + set_render_size(screen, width, height); + + const three_scene = deserialize_scene(scenes, screen); + + start_renderloop(three_scene); + + canvas_width.on((w_h) => { + // `renderer.setSize` correctly updates `canvas` dimensions + set_render_size(screen, ...w_h); + }); return renderer; } @@ -439,14 +498,24 @@ function set_picking_uniforms( return next_id; } -export function pick_native(scene, x, y, w, h) { - const { renderer, picking_target } = scene.screen; +/** + * + * @param {*} scene + * @param {*} x in scene unitless pixel space + * @param {*} y in scene unitless pixel space + * @param {*} w in scene unitless pixel space + * @param {*} h in scene unitless pixel space + * @returns + */ +export function pick_native(scene, _x, _y, _w, _h) { + const { renderer, picking_target, px_per_unit } = scene.screen; + [_x, _y, _w, _h] = [_x, _y, _w, _h].map((x) => Math.ceil(x * px_per_unit)); + const [x, y, w, h] = [_x, _y, _w, _h]; // render the scene renderer.setRenderTarget(picking_target); set_picking_uniforms(scene, 1, true); render_scene(scene, true); renderer.setRenderTarget(null); // reset render target - const nbytes = w * h * 4; const pixel_bytes = new Uint8Array(nbytes); //read the pixel @@ -458,6 +527,8 @@ export function pick_native(scene, x, y, w, h) { h, // height pixel_bytes ); + + const picked_plots = {}; const picked_plots_array = []; @@ -483,8 +554,8 @@ export function pick_native(scene, x, y, w, h) { } export function pick_closest(scene, xy, range) { - const { picking_target } = scene.screen; - const { width, height } = picking_target; + const { renderer } = scene.screen; + const [ width, height ] = [renderer._width, renderer._height]; if (!(1.0 <= xy[0] <= width && 1.0 <= xy[1] <= height)) { return [null, 0]; @@ -494,6 +565,7 @@ export function pick_closest(scene, xy, range) { const y0 = Math.max(1, xy[1] - range); const x1 = Math.min(width, Math.floor(xy[0] + range)); const y1 = Math.min(height, Math.floor(xy[1] + range)); + const dx = x1 - x0; const dy = y1 - y0; const [plot_data, _] = pick_native(scene, x0, y0, dx, dy); @@ -518,8 +590,8 @@ export function pick_closest(scene, xy, range) { } export function pick_sorted(scene, xy, range) { - const { picking_target } = scene.screen; - const { width, height } = picking_target; + const { renderer } = scene.screen; + const [width, height] = [renderer._width, renderer._height]; if (!(1.0 <= xy[0] <= width && 1.0 <= xy[1] <= height)) { return null; @@ -532,11 +604,11 @@ export function pick_sorted(scene, xy, range) { const dx = x1 - x0; const dy = y1 - y0; + const [plot_data, selected] = pick_native(scene, x0, y0, dx, dy); if (selected.length == 0) { return null; } - const plot_matrix = plot_data.data; const distances = selected.map((x) => range ^ 2); const x = xy[0] + 1 - x0; @@ -545,6 +617,9 @@ export function pick_sorted(scene, xy, range) { for (let i = 1; i <= dx; i++) { for (let j = 1; j <= dx; j++) { const d = (x - i) ^ (2 + (y - j)) ^ 2; + if (plot_matrix.length <= pindex) { + continue; + } const [plot_uuid, index] = plot_matrix[pindex]; pindex = pindex + 1; const plot_index = selected.findIndex( @@ -553,6 +628,7 @@ export function pick_sorted(scene, xy, range) { if (plot_index >= 0 && d < distances[plot_index]) { distances[plot_index] = d; } + } } @@ -583,17 +659,17 @@ export function register_popup(popup, scene, plots_to_pick, callback) { } const { canvas } = scene.screen; canvas.addEventListener("mousedown", (event) => { - if (!popup.classList.contains("show")) { - popup.classList.add("show"); - } - popup.style.left = event.pageX + "px"; - popup.style.top = event.pageY + "px"; - const [x, y] = event2scene_pixel(scene, event); + const [x, y] = events2unitless(scene.screen, event); const [_, picks] = pick_native(scene, x, y, 1, 1); if (picks.length == 1) { const [plot, index] = picks[0]; if (plots_to_pick.has(plot.plot_uuid)) { const result = callback(plot, index); + if (!popup.classList.contains("show")) { + popup.classList.add("show"); + } + popup.style.left = event.pageX + "px"; + popup.style.top = event.pageY + "px"; if (typeof result === "string" || result instanceof String) { popup.innerText = result; } else { @@ -624,7 +700,7 @@ window.WGL = { plot_cache, delete_scenes, create_scene, - event2scene_pixel, + events2unitless, on_next_insert, register_popup, render_scene, @@ -643,6 +719,6 @@ export { plot_cache, delete_scenes, create_scene, - event2scene_pixel, + events2unitless, on_next_insert, }; diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 32360727453..22135765afd 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -1,13 +1,13 @@ using FileIO using WGLMakie, Makie, Test using WGLMakie.JSServe -import Electron using ReferenceTests +import Electron @testset "mimes" begin Makie.inline!(true) f, ax, pl = scatter(1:4) - @testset for mime in WGLMakie.WEB_MIMES + @testset for mime in Makie.WEB_MIMES @test showable(mime(), f) end # I guess we explicitely don't say we can show those since it's highly Inefficient compared to html @@ -31,20 +31,16 @@ excludes = Set([ "FEM mesh 2D", "FEM polygon 2D", # missing transparency & image - "Wireframe of a Surface", "Image on Surface Sphere", - "Surface with image", # Marker size seems wrong in some occasions: "Hbox", "UnicodeMarker", # Not sure, looks pretty similar to me! Maybe blend mode? "Test heatmap + image overlap", - "heatmaps & surface", - "OldAxis + Surface", + # "heatmaps & surface", # TODO: fix direct NaN -> nancolor conversion "Order Independent Transparency", "Record Video", "fast pixel marker", - "Animated surface and wireframe", "Array of Images Scatter", "Image Scatter different sizes", "scatter with stroke", @@ -55,17 +51,36 @@ excludes = Set([ ]) Makie.inline!(Makie.automatic) +edisplay = JSServe.use_electron_display(devtools=true) @testset "refimages" begin WGLMakie.activate!() - d = JSServe.use_electron_display() ReferenceTests.mark_broken_tests(excludes) recorded_files, recording_dir = @include_reference_tests "refimages.jl" missing_images, scores = ReferenceTests.record_comparison(recording_dir) ReferenceTests.test_comparison(scores; threshold = 0.032) - end @testset "memory leaks" begin - GC.gc(true) - @test Base.summarysize(WGLMakie.TEXTURE_ATLAS) / 10^6 < 9 + Makie.CURRENT_FIGURE[] = nothing + app = App(nothing) + display(edisplay, app) + GC.gc(true); + # Somehow this may take a while to get emptied completely + JSServe.wait_for(() -> (GC.gc(true);isempty(run(edisplay.window, "Object.keys(WGL.plot_cache)")));timeout=20) + wgl_plots = run(edisplay.window, "Object.keys(WGL.scene_cache)") + @test isempty(wgl_plots) + + session = edisplay.browserdisplay.handler.session + session_size = Base.summarysize(session) / 10^6 + texture_atlas_size = Base.summarysize(WGLMakie.TEXTURE_ATLAS) / 10^6 + @show session_size texture_atlas_size + @test session_size / 10^6 < 6 + @test texture_atlas_size < 6 + s_keys = "Object.keys(JSServe.Sessions.SESSIONS)" + JSServe.wait_for(() -> (GC.gc(true); 2 == length(run(edisplay.window, s_keys))); timeout=30) + js_sessions = run(edisplay.window, "JSServe.Sessions.SESSIONS") + js_objects = run(edisplay.window, "JSServe.Sessions.GLOBAL_OBJECT_CACHE") + # @test Set([app.session[].id, app.session[].parent.id]) == keys(js_sessions) + # we used Retain for global_obs, so it should stay as long as root session is open + @test keys(js_objects) == Set([WGLMakie.TEXTURE_ATLAS.id]) end diff --git a/docs/explanations/backends/cairomakie.md b/docs/explanations/backends/cairomakie.md index d31748bcd57..2619d3f79c8 100644 --- a/docs/explanations/backends/cairomakie.md +++ b/docs/explanations/backends/cairomakie.md @@ -24,31 +24,6 @@ CairoMakie.activate!(type = "png") CairoMakie.activate!(type = "svg") ``` -#### Resolution Scaling - -When you save a CairoMakie figure, you can change the mapping from figure resolution to pixels (when saving to png) or points (when saving to svg or pdf). -This way you can easily scale the resulting image up or down without having to change any plot element sizes. - -Just specify `pt_per_unit` when saving vector formats and `px_per_unit` when saving pngs. -`px_per_unit` defaults to 1 and `pt_per_unit` defaults to 0.75. -When embedding svgs in websites, `1px` is equivalent to `0.75pt`. -This means that by default, saving a png or an svg results in an embedded image of the same apparent size. -If you require an exact size in `pt`, consider setting `pt_per_unit = 1`. - -Here's an example: - -```julia -fig = Figure(resolution = (800, 600)) - -save("normal.pdf", fig) # size = 600 x 450 pt -save("larger.pdf", fig, pt_per_unit = 2) # size = 1600 x 1200 pt -save("smaller.pdf", fig, pt_per_unit = 0.5) # size = 400 x 300 pt - -save("normal.png", fig) # size = 800 x 600 px -save("larger.png", fig, px_per_unit = 2) # size = 1600 x 1200 px -save("smaller.png", fig, px_per_unit = 0.5) # size = 400 x 300 px -``` - #### Z-Order CairoMakie as a 2D engine has no concept of z-clipping, therefore its 3D capabilities are quite limited. @@ -62,7 +37,7 @@ By setting the `rasterize` attribute of a plot, you can tell CairoMakie that thi Assuming that you have a `Plot` object `plt`, you can set `plt.rasterize = true` for simple rasterization, or you can set `plt.rasterize = scale::Int`, where `scale` represents the scaling factor for the image surface. -For example, if your Scene's resolution is `(800, 600)`, by setting `scale=2`, the rasterized image will have a resolution of `(1600, 1200)`. +For example, if your Scene's size is `(800, 600)`, by setting `scale=2`, the rasterized image embedded in the vector graphic will have a resolution of `(1600, 1200)`. You can deactivate this rasterization by setting `plt.rasterize = false`. diff --git a/docs/explanations/backends/glmakie.md b/docs/explanations/backends/glmakie.md index f26a77c70ed..3ab4785869a 100644 --- a/docs/explanations/backends/glmakie.md +++ b/docs/explanations/backends/glmakie.md @@ -15,6 +15,50 @@ println("~~~") ``` \textoutput{docs} +#### Window Scaling + +The sizes of figures are given in display-independent "logical" dimensions, and the +GLMakie backend will scale the size of the displayed window on HiDPI/Retina displays +automatically. +For example, the default `size = (800, 600)` will be shown in a 1600 × 1200 window +on a HiDPI display which is configured with a 200% scaling factor. + +The scaling factor may be overridden by displaying the figure with a different +`scalefactor` value: +```julia +fig = Figure(size = (800, 600)) +# ... +display(fig, scalefactor = 1.5) +``` + +If the scale factor is not changed from its default automatic configuration, the window +will be resized to maintain its apparent size when moved across displays with different +scaling factors on Windows and OSX. +(Independent scaling factors are not supported by X11, and at this time the underlying +GLFW library is not compiled with Wayland support.) + +#### Resolution Scaling + +Related to the window scaling factor, the mapping from figure sizes and positions to pixels +can be scaled to achieve HiDPI/Retina resolution renderings. +The resolution scaling defaults to the same factor as the window scaling, but it may +be independently overridden with the `px_per_unit` argument when showing a figure: +```julia +fig = Figure(size = (800, 600)) +# ... +display(fig, px_per_unit = 2) +``` + +The resolution scale factor may also be changed when saving pngs: +```julia +save("hires.png", fig, px_per_unit = 2) # 1600 × 1200 px png +save("lores.png", fig, px_per_unit = 0.5) # 400 × 300 px png +``` +If a script may run in interactive environments where the native screen DPI can vary, +you may want to explicitly set `px_per_unit = 1` when saving figures to ensure consistency +of results. + + #### Multiple Windows GLMakie has experimental support for displaying multiple independent figures (or scenes). To open a new window, use `display(GLMakie.Screen(), figure_or_scene)`. To close all windows, use `GLMakie.closeall()`. diff --git a/docs/explanations/backends/rprmakie.md b/docs/explanations/backends/rprmakie.md index bbd4f31903c..8120a42dcc2 100644 --- a/docs/explanations/backends/rprmakie.md +++ b/docs/explanations/backends/rprmakie.md @@ -94,7 +94,7 @@ using Colors: N0f8 radiance = 500 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] -fig = Figure(; resolution=(1500, 700)); +fig = Figure(; size=(1500, 700)); ax = LScene(fig[1, 1]; show_axis=false, scenekw=(; lights=lights)) screen = RPRMakie.Screen(ax.scene; plugin=RPR.Northstar, iterations=400) @@ -178,7 +178,7 @@ function glow_material(data_normed) end RPRMakie.activate!(iterations=32, plugin=RPR.Northstar) -fig = Figure(; resolution=(2000, 800)) +fig = Figure(; size=(2000, 800)) radiance = 30000 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(0, 100, 100), RGBf(radiance, radiance, radiance))] @@ -232,7 +232,7 @@ radiance = 500 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] -fig = Figure(; resolution=(1500, 1000)) +fig = Figure(; size=(1500, 1000)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(; lights=lights)) screen = RPRMakie.Screen(size(ax.scene); plugin=RPR.Tahoe) material = RPR.UberMaterial(screen.matsys) @@ -397,7 +397,7 @@ lights = [ EnvironmentLight(1.5, rotl90(load(assetpath("sunflowers_1k.hdr"))')), PointLight(Vec3f(50, 0, 200), RGBf(radiance, radiance, radiance*1.1)), ] -s = Scene(resolution=(500, 500), lights=lights) +s = Scene(size=(500, 500), lights=lights) cam3d!(s) c = cameracontrols(s) @@ -491,7 +491,7 @@ earth_img = load(Downloads.download("https://upload.wikimedia.org/wikipedia/comm # the actual plot ! RPRMakie.activate!(; iterations=100) scene = with_theme(theme_dark()) do - fig = Figure(; resolution=(1000, 1000)) + fig = Figure(; size=(1000, 1000)) radiance = 30 lights = [EnvironmentLight(0.5, load(RPR.assetpath("starmap_4k.tif"))), PointLight(Vec3f(1, 1, 3), RGBf(radiance, radiance, radiance))] diff --git a/docs/explanations/backends/wglmakie.md b/docs/explanations/backends/wglmakie.md index 39c05e215bd..599c22b6776 100644 --- a/docs/explanations/backends/wglmakie.md +++ b/docs/explanations/backends/wglmakie.md @@ -378,7 +378,7 @@ end App() do session::Session # We can now use this wherever we want: - fig = Figure(resolution=(200, 200)) + fig = Figure(size=(200, 200)) contour(fig[1,1], rand(4,4)) card = GridCard( Slider(1:100), diff --git a/docs/explanations/cameras.md b/docs/explanations/cameras.md index f935c92c29c..54d87983041 100644 --- a/docs/explanations/cameras.md +++ b/docs/explanations/cameras.md @@ -1,13 +1,15 @@ # Cameras -A `Camera` is simply a viewport through which the Scene is visualized. `Makie` offers 2D and 3D projections, and 2D plots can be projected in 3D! +A `Camera` is simply a viewport through which the Scene is visualized. `Makie` offers 2D and 3D projections, and 2D plots can be projected in 3D! -To specify the camera you want to use for your Scene, you can set the `camera` attribute. Currently, we offer four types of camera: +To specify the camera you want to use for your Scene, you can set the `camera` attribute. Currently, we offer the following cameras/constructors \apilink{campixel!} +\apilink{cam_relative!} \apilink{cam2d!} -`cam3d!` -`cam3d_cad!` +\apilink{Camera3D} +\apilink{cam3d!} +\apilink{cam3d_cad!} which will mutate the camera of the Scene into the specified type. @@ -15,6 +17,10 @@ which will mutate the camera of the Scene into the specified type. The pixel camera (\apilink{campixel!(scene)}) projects the scene in pixel space, i.e. each integer step in the displayed data will correspond to one pixel. There are no controls for this camera. The clipping limits are set to `(-10_000, 10_000)`. +## Relative Camera + +The relative camera (\apilink{cam_relative!(scene)}) projects the scene into a 0..1 by 0..1 space. There are no controls for this camera. The clipping limits are set to `(-10_000, 10_000)`. + ## 2D Camera The 2D camera (\apilink{cam2d!(scene)}) uses an orthographic projection with a fixed rotation and aspect ratio. You can set the following attributes via keyword arguments in `cam2d!` or by accessing the camera struct `cam = cameracontrols(scene)`: @@ -30,6 +36,61 @@ Note that this camera is not used by `Axis`. It is used, by default, for 2D `LSc {{doc Camera3D}} +`cam3d!` and `cam3d_cad!` but create a `Camera3D` with some specific options. + +## Example - Visualizing the cameras view box + +```julia +using GeometryBasics, LinearAlgebra + +function frustum_snapshot(cam) + r = Rect3f(Point3f(-1, -1, -1), Vec3f(2, 2, 2)) + rect_ps = coordinates(r) .|> Point3f + insert!(rect_ps, 13, Point3f(1, -1, 1)) # fix bad line + + inv_pv = inv(cam.projectionview[]) + return map(rect_ps) do p + p = inv_pv * to_ndim(Point4f, p, 1) + return p[Vec(1,2,3)] / p[4] + end +end + + +ex = Point3f(1,0,0) +ey = Point3f(0,1,0) +ez = Point3f(0,0,1) + +fig = Figure() +scene = LScene(fig[1, 1]) +cc = Makie.Camera3D(scene.scene, projectiontype = Makie.Perspective) + +linesegments!(scene, Rect3f(Point3f(-1), Vec3f(2)), color = :black) +linesegments!(scene, + [-ex, ex, -ey, ey, -ez, ez], + color = [:red, :red, :green, :green, :blue, :blue] +) +center!(scene.scene) + +cam = scene.scene.camera +eyeposition = cc.eyeposition +lookat = cc.lookat +frustum = map(pv -> frustum_snapshot(cam), cam.projectionview) + +scene = LScene(fig[1, 2]) +_cc = Makie.Camera3D(scene.scene, projectiontype = Makie.Orthographic) +lines!(scene, frustum, color = :blue, linestyle = :dot) +scatter!(scene, eyeposition, color = :black) +scatter!(scene, lookat, color = :black) + +linesegments!(scene, + [-ex, ex, -ey, ey, -ez, ez], + color = [:red, :red, :green, :green, :blue, :blue] +) +linesegments!(scene, Rect3f(Point3f(-1), Vec3f(2)), color = :black) + +fig +``` + ## General Remarks To force a plot to be visualized in 3D, you can set the limits to have a nonzero \(z\)-axis interval, or ensure that a 3D camera type is used. diff --git a/docs/explanations/colors.md b/docs/explanations/colors.md index 77b39c0e6ea..995ee1da1a6 100644 --- a/docs/explanations/colors.md +++ b/docs/explanations/colors.md @@ -36,7 +36,7 @@ theme = Attributes( with_theme(theme) do - f = Figure(resolution = (800, 1200)) + f = Figure(size = (800, 1200)) ax = Axis(f[1, 1], xautolimitmargin = (0.2, 0.2), yautolimitmargin = (0.1, 0.1)) hidedecorations!(ax) hidespines!(ax) diff --git a/docs/explanations/events.md b/docs/explanations/events.md index d477c32bd88..99ff8e4fc22 100644 --- a/docs/explanations/events.md +++ b/docs/explanations/events.md @@ -376,6 +376,8 @@ Furthermore you can wrap any of the above in `Exclusively` to discard matches wh - `hotkey = Keyboard.left_control & Keyboard.a` is equivalent to `(Keyboard.left_control, Keyboard.a)` - `hotkey = (Keyboard.left_control | Keyboard.right_control) & Keyboard.a` allows either left or right control with a. +Note that the way we used `ispressed` above, the condition will be true for "press" and "repeat" events. You can further restrict to one or the other by checking `event.action`. If you wish to react to a "release" event, you will need to pass `event.key`/`event.button` as a third argument to `ispressed(fig, hotkey, event.key)`. This will tell `ispressed` to assume the key or button is pressed if it is part of the hotkey. + ## Interactive Widgets Makie has a couple of useful interactive widgets like sliders, buttons and menus, which you can learn about in the \myreflink{Blocks} section. diff --git a/docs/explanations/faq.md b/docs/explanations/faq.md index 261d1fcc68d..58bb69d0aef 100644 --- a/docs/explanations/faq.md +++ b/docs/explanations/faq.md @@ -118,13 +118,12 @@ f ### Columns or rows are shrunk to the size of Text or another element Columns or rows that have size `Auto(true)` try to determine the width or height of all -single-spanned elements that are placed in them, and if any elements report their -size the row or column will shrink to the maximum reported size. This is so smaller +single-spanned elements that are placed in them, and if any elements "tell" the layout their own height or width, +the row or column will shrink to the maximum reported size. This is so smaller elements with a known size take as little space as needed. But if there is other content in the row that should take more space, you can give the offending element -the attribute `tellheight = false` or `tellwidth = false`. This way, its own size -can be determined automatically, but -it doesn't report it to the row or column of the layout. Alternatively, you can set the size +the attribute `tellheight = false` or `tellwidth = false`. This way, its own height +or width doesn't influence the automatic sizing of the layout. Alternatively, you can set the size of that row or column to `Auto(false)` (or any other value than `Auto(true)`). \begin{examplefigure}{svg = true} @@ -135,8 +134,8 @@ f = Figure() Axis(f[1, 1], title = "Shrunk") Axis(f[2, 1], title = "Expanded") -Label(f[1, 2], "tellheight = true", tellheight = true) -Label(f[2, 2], "tellheight = false", tellheight = false) +Label(f[1, 2], "This Label has the setting\ntellheight = true\ntherefore the row it is in has\nadjusted to match its height.", tellheight = true) +Label(f[2, 2], "This Label has the setting\ntellheight = false.\nThe row it is in can use\nall the remaining space.", tellheight = false) f ``` @@ -163,7 +162,7 @@ using CairoMakie set_theme!(backgroundcolor = :gray90) -f = Figure(resolution = (800, 600)) +f = Figure(size = (800, 600)) for i in 1:3, j in 1:3 ax = Axis(f[i, j], title = "$i, $j", width = 100, height = 100) diff --git a/docs/explanations/figure.md b/docs/explanations/figure.md index 8117bcc4fbf..8d7e878191a 100644 --- a/docs/explanations/figure.md +++ b/docs/explanations/figure.md @@ -3,14 +3,14 @@ The `Figure` object contains a top-level `Scene` and a `GridLayout`, as well as a list of blocks that have been placed into it, like `Axis`, `Colorbar`, `Slider`, `Legend`, etc. -## Creating a `Figure` +## Creating a Figure You can create a figure explicitly with the `Figure()` function, and set attributes of the underlying scene. -The most important one of which is the `resolution`. +The most important one of which is the `size`. ```julia f = Figure() -f = Figure(resolution = (600, 400)) +f = Figure(size = (600, 400)) ``` A figure is also created implicitly when you use simple, non-mutating plotting commands like `plot()`, `scatter()`, `lines()`, etc. @@ -31,12 +31,12 @@ figure, = scatter(rand(100, 2)) You can pass arguments to the created figure in a dict-like object to the special `figure` keyword: ```julia -scatter(rand(100, 2), figure = (resolution = (600, 400),)) +scatter(rand(100, 2), figure = (size = (600, 400),)) ``` -## Placing blocks into a `Figure` +## Placing Blocks into a Figure -All blocks take their parent figure as the first argument, then you can place them in the figure layout via indexing syntax. +All Blocks take their parent figure as the first argument, then you can place them in the figure layout via indexing syntax. ```julia f = Figure() @@ -166,3 +166,53 @@ ax = f[1, 1] = Axis(f) contents(f[1, 1]) == [ax] content(f[1, 1]) == ax ``` + +## Figure size and units + +In Makie, figure size and attributes like line widths, font sizes, scatter marker extents, or layout column and row gaps are usually given as plain numbers, without an explicit unit attached. +What does it mean to have a `Figure` with `size = (600, 450)`, a line with `linewidth = 10` or a column gap of `30`? + +The first underlying idea is that, no matter what your final output format is, these numbers are _relative_. +You can expect a `linewidth = 10` to cover 1/60th of the width `600` of the `Figure` and a column gap of `30` to span 1/20th of the Figure. +This holds, no matter if you later export that `Figure` as an image made out of pixels, or as a vector graphic that doesn't have pixels at all. + +The second idea is that, given some `Figure`, we want to be able to export an image at arbitrary resolution, or a vector graphic at any size from it, as long as the relative sizes of all elements stay intact. +So we need to _translate_ our abstract sizes to real sizes when we render. +In Makie, this is done with two scaling factors: `px_per_unit` for images and `pt_per_unit` for vector graphics. + +A line with `linewidth = 10` will be 10 pixels wide if rendered to an image file with `px_per_unit = 1`. It will be 5 pixels wide if `px_per_unit = 0.5` and 20 pixels if `px_per_unit = 2`. A `Figure` with `size = (600, 450)` will have 600 x 450 pixels when exported with `px_per_unit = 1`, 300 x 225 with `px_per_unit = 0.5` and 1200 x 900 with `px_per_unit = 2`. + +It works exactly the same for vector graphics, just with a different target unit. A `pt` or point is a typographic unit that is defined as 1/72 of an inch, which comes out to about 0.353 mm. A line with `linewidth = 10` will be 10 points wide if rendered to an svg file with `pt_per_unit = 1`, it will be 5 points wide for `pt_per_unit = 0.5` and 20 points wide if `pt_per_unit = 2`. A `Figure` with `size = (600, 450)` will be 600 x 450 points in size when exported with `pt_per_unit = 1`, 300 x 225 with `pt_per_unit = 0.5` and 1200 x 900 with `pt_per_unit = 2`. + +### Defaults of `px_per_unit` and `pt_per_unit` + +What are the default values of `px_per_unit` and `pt_per_unit` in each Makie backend, and why are they set that way? + +Let us start with `pt_per_unit` because this value is only relevant for one backend, which is CairoMakie. +The default value in CairoMakie is `pt_per_unit = 0.75`. So if you `save(figure, "output.svg")` a `Figure` with `size = (600, 450)`, this comes out as a vector graphic that is 450 x 337.5 pt large. + +Why 0.75 and not simply 1? This has to do with web standards and device-independent pixels. Websites mix vector graphics and images, so they need some way to relate the sizes of both types to each other. In principle, a pixel in an image doesn't have a real-world width. But you don't want the images on your site to shrink relative to the other content when device pixels are small, or grow when device pixels are large. So web browsers don't directly map image pixels to device pixels. Instead, they use a concept called device-independent pixels. If you place an image with 600 x 450 pixels in a website, this image is interpreted by default to be 600 x 450 device-independent pixels wide. One device-independent pixel is defined to be 0.75 pt wide, that's where the factor 0.75 comes in. So an image with 600 x 450 device-independent pixels is the same apparent size as a vector graphic with size 450 x 337.5 pt. On high-resolution screens, browsers then simply render one device-independent pixel with multiple device pixels (for example 2x2 on an Apple Retina display) so that content stays at readable sizes and doesn't look tiny. + +For Makie, we decided that we want our abstract units to match device-independent pixels when used in web contexts, because that's very convenient and easy to predict for the end user. If you have a Jupyter or Pluto notebook, it's nice if a `Figure` comes out at the same apparent size, no matter if you're currently in CairoMakie's svg mode, or in the bitmap mode of any backend. Therefore, we annotate images with the original `Figure` size in device-independent pixels, so they are of the same apparent size, no matter what the `px_per_unit` value and therefore the effective pixel size is. And we give svg files the default scaling factor of 0.75 so that svgs always match images in apparent size. + +Now let us look at the default values for `px_per_unit`. In CairoMakie, the default is `px_per_unit = 2`. This means, a `Figure` with `size = (600, 450)` will be rendered as a 1200 x 900 pixel image. The reason it isn't `px_per_unit = 1` is that CairoMakie plots are often embedded in notebooks or websites, or looked at in image viewers or IDEs like VSCode. On websites, you don't know in advance what the pixel density of a reader's display is going to be. And in image viewers and IDEs, people like to zoom in to look at details. To cover these use cases by default, we decided `px_per_unit = 2` is a good compromise between sharp resolution and appropriate file size. Again, the _apparent_ size of output images in notebooks and websites (wherever the `MIME"text/html"` type is used) depends only on the `size`, because the output images are embedded with ` S.Density(x * randn(200) .+ 3x, color=:y), 1:5) +brain_mesh = S.Mesh(brain, colormap=:Spectral, color=[tri[1][2] for tri in brain for i in 1:3]) +volcano_contour = S.Contourf(volcano; colormap=:inferno) +cube_contour = S.Contour(cube, alpha=0.5) + +ax_densities = S.Axis(; plots=density_plots, yautolimitmargin = (0, 0.1)) +ax_volcano = S.Axis(; plots=[volcano_contour]) +ax_brain = S.Axis3(; plots=[brain_mesh], protrusions = (50, 20, 10, 0)) +ax_cube = S.Axis3(; plots=[cube_contour], protrusions = (50, 20, 10, 0)) + +spec_column_vector = S.GridLayout([ax_densities, ax_volcano, ax_brain]); +spec_matrix = S.GridLayout([ax_densities ax_volcano; ax_brain ax_cube]); +spec_row = S.GridLayout([spec_column_vector spec_matrix], colsizes = [Auto(), Auto(4)]) + +f, ax, pl = plot(S.GridLayout(spec_row); figure = (; fontsize = 10)) +``` +\end{examplefigure} + +## Advanced spec layouting + +If you need even more control, you can pass the position of each object in your layout to `S.GridLayout` directly. +These positions are specified as a tuple of `(rows, columns [, side])` where `side` is `Inside()` by default. +For `rows` and `columns` you can either use integers like `2`, ranges like `1:3` or the colon operator `:` which spans across all rows or columns that are specified for other elements. +Rows and columns start at `1` by default but you can also use numbers lower than `1` if necessary. + +\begin{examplefigure}{svg = true} +```julia +using CairoMakie +import Makie.SpecApi as S +Makie.inline!(true) # hide +CairoMakie.activate!() # hide + +plot( + S.GridLayout([ + (1, 1) => S.Axis(), + (1, 2) => S.Axis(), + (2, :) => S.Axis(), + (2, 2, Right()) => S.Box(), + (2, 2, Right()) => S.Label( + text = "Label", + rotation = pi/2, + padding = (10, 10, 10, 10) + ), + ]) +) +``` +\end{examplefigure} + +You can also use manual positions with nested `GridLayout`s. + +\begin{examplefigure}{} +```julia +using CairoMakie +import Makie.SpecApi as S +Makie.inline!(true) # hide +CairoMakie.activate!() # hide + +plot(S.GridLayout([ + (1, 1) => S.Axis(), + (1, 2) => S.Axis(), + (2, :) => S.GridLayout(fill(S.Axis(), 1, 3)), +])) +``` +\end{examplefigure} + +Here are all the keyword arguments that `S.GridLayout` accepts. + +```julia +S.GridLayout([...], + colsizes = [Auto(), Auto(), 300], + rowsizes = [Relative(0.4), Relative(0.6)], + colgaps, + rowgaps, + alignmode, + halign, + valign, + tellheight, + tellwidth, +) +``` + +## Using specs in `convert_arguments` + +!!! warning + It's not decided yet how to forward keyword arguments from `plots(...; kw...)` to `convert_arguments` for the SpecApi in a more convenient and performant way. Until then, you need to mark attributes you want to use in `convert_arguments` with `Makie.used_attributes`, but this will completely redraw the entire spec on change of any attribute. We also may require users to overload a different function in future versions. + +You can overload `convert_arguments` and return an array of `PlotSpecs` or a `GridLayoutSpec`. +The main difference between those is, that returning an array of `PlotSpecs` may be plotted like any recipe into axes, while overloads returning `GridLayoutSpec` may not. + +## `convert_arguments` for `GridLayoutSpec` + +In this example, we overload `convert_arguments` for a custom type to create facet grids easily. + +\begin{examplefigure}{svg = true} +```julia +using CairoMakie +import Makie.SpecApi as S +CairoMakie.activate!() # hide + +# Our custom type we want to write a conversion method for +struct PlotGrid + nplots::Tuple{Int,Int} +end + +# If we want to use the `color` attribute in the conversion, we have to +# mark it via `used_attributes` +Makie.used_attributes(::PlotGrid) = (:color,) + +# The conversion method creates a grid of `Axis` objects with `Lines` plot inside +# We restrict to Plot{plot}, so that only `plot(PlotGrid(...))` works, but not e.g. `scatter(PlotGrid(...))`. +function Makie.convert_arguments(::Type{Plot{plot}}, obj::PlotGrid; color=:black) + axes = [ + S.Axis(plots=[S.Lines(cumsum(randn(1000)); color=color)]) + for i in 1:obj.nplots[1], + j in 1:obj.nplots[2] + ] + return S.GridLayout(axes) +end + +# Now, when we plot `PlotGrid` we get a whole facet layout +plot(PlotGrid((3, 4))) +``` +\end{examplefigure} + +We can also plot into existing `Figure`s with our new `plot` method: + +\begin{examplefigure}{svg = true} +```julia +f = Figure() +plot(f[1, 1], PlotGrid((2, 2)); color=Cycled(1)) +plot(f[1, 2], PlotGrid((3, 2)); color=Cycled(2)) +f +``` +\end{examplefigure} + +## `convert_arguments` for `PlotSpec`s + +We can return a vector of `PlotSpec`s from `convert_arguments` which allows us to dynamically choose the plot objects we want to add given the input data. +While you could choose plot types based on input data with the old recipe API as well, this did not easily work for observable updates that changed these plot types in an existing figure. +For this, users had to do tedious manual bookkeeping which is now abstracted away. + +Note, that this method currently doesn't allow to forward keyword arguments from the `plot` command to `convert_arguments`, so we put the plot arguments into the `LineScatter` object in the following example: + +\begin{examplefigure}{} +```julia +using CairoMakie +import Makie.SpecApi as S +using Random +CairoMakie.activate!() # hide + +Random.seed!(123) + +# define a struct for `convert_arguments` +struct CustomMatrix + data::Matrix{Float32} + style::Symbol + kw::Dict{Symbol,Any} +end +CustomMatrix(data; style, kw...) = CustomMatrix(data, style, Dict{Symbol,Any}(kw)) + +function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::CustomMatrix) + plots = PlotSpec[] + if obj.style === :heatmap + push!(plots, S.Heatmap(obj.data; obj.kw...)) + elseif obj.style === :contourf + push!(plots, S.Contourf(obj.data; obj.kw...)) + end + max_position = Tuple(argmax(obj.data)) + push!(plots, S.Scatter(max_position; markersize = 30, strokecolor = :white, color = :transparent, strokewidth = 4)) + return plots +end + +data = randn(30, 30) + +f = Figure() +ax = Axis(f[1, 1]) +# We can either plot into an existing Axis +plot!(ax, CustomMatrix(data, style = :heatmap, colormap = :Blues)) +# Or create a new one automatically as we are used to from the standard API +plot(f[1, 2], CustomMatrix(data, style = :contourf, colormap = :inferno)) +f +``` +\end{examplefigure} + + +## Interactive example + +The SpecApi is geared towards dashboards and interactively creating complex plots. +Here is an example using a `Slider` and a `Menu`, to visualize a fake simulation: + +~~~ + +~~~ +```julia:simulation +using GLMakie +import Makie.SpecApi as S +GLMakie.activate!() # hide + +struct MySimulation + plottype::Symbol + arguments::AbstractVector +end + +function Makie.convert_arguments(::Type{<:AbstractPlot}, sim::MySimulation) + return map(enumerate(sim.arguments)) do (i, data) + return PlotSpec(sim.plottype, data) + end +end +f = Figure() +s = Slider(f[1, 1], range=1:10) +m = Menu(f[1, 2], options=[:Scatter, :Lines, :BarPlot]) +sim = lift(s.value, m.selection) do n_plots, p + Random.seed!(123) + args = [cumsum(randn(100)) for i in 1:n_plots] + return MySimulation(p, args) +end +ax, pl = plot(f[2, :], sim) +tight_ticklabel_spacing!(ax) +# lower priority to make sure the call back is always called last +on(sim; priority=-1) do x + autolimits!(ax) +end + +record(f, "interactive_specapi.mp4", framerate=1) do io + pause = 0.1 + m.i_selected[] = 1 + for i in 1:4 + set_close_to!(s, i) + sleep(pause) + recordframe!(io) + end + m.i_selected[] = 2 + sleep(pause) + recordframe!(io) + for i in 5:7 + set_close_to!(s, i) + sleep(pause) + recordframe!(io) + end + m.i_selected[] = 3 + sleep(pause) + recordframe!(io) + for i in 7:10 + set_close_to!(s, i) + sleep(pause) + recordframe!(io) + end +end +``` +~~~ + +~~~ + +\video{interactive_specapi, autoplay = true} diff --git a/docs/explanations/theming.md b/docs/explanations/theming.md index 95cd152857a..b4204170118 100644 --- a/docs/explanations/theming.md +++ b/docs/explanations/theming.md @@ -279,7 +279,7 @@ CairoMakie.activate!() # hide set_theme!() # hide -f = Figure(resolution = (800, 800)) +f = Figure(size = (800, 800)) Axis(f[1, 1], title = "Default cycle palette") diff --git a/docs/explanations/transparency.md b/docs/explanations/transparency.md index 6347938c2c7..03f49a05685 100644 --- a/docs/explanations/transparency.md +++ b/docs/explanations/transparency.md @@ -37,7 +37,7 @@ The color generated from two overlapping transparent objects depends on their or using CairoMakie CairoMakie.activate!() # hide -scene = Scene(resolution = (400, 275)) +scene = Scene(size = (400, 275)) campixel!(scene) scatter!( scene, [100, 200, 300], [100, 100, 100], @@ -66,8 +66,8 @@ CairoMakie.activate!() # hide fig = Figure() ax = LScene(fig[1, 1], show_axis=false) -p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = false) -p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = false) +p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = NoShading) +p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = NoShading) rotate!(p1, Vec3f(0, 1, 0), 0.1) rotate!(p2, Vec3f(0, 1, 0), -0.1) fig @@ -81,8 +81,8 @@ GLMakie.activate!() # hide fig = Figure() ax = LScene(fig[1, 1], show_axis=false) -p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = false) -p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = false) +p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = NoShading) +p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = NoShading) rotate!(p1, Vec3f(0, 1, 0), 0.1) rotate!(p2, Vec3f(0, 1, 0), -0.1) fig @@ -105,9 +105,9 @@ GLMakie.activate!() # hide fig = Figure() ax = LScene(fig[1, 1], show_axis=false) -p1 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = false, transparency = true) -p2 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:blue, 0.5), shading = false, transparency = true) -p3 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = false, transparency = true) +p1 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = NoShading, transparency = true) +p2 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:blue, 0.5), shading = NoShading, transparency = true) +p3 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = NoShading, transparency = true) for (dz, p) in zip((-1, 0, 1), (p1, p2, p3)) translate!(p, 0, 0, dz) end @@ -124,18 +124,18 @@ Being an approximate scheme OIT has some strengths and weaknesses. There are two using GLMakie GLMakie.activate!() # hide -fig = Figure(resolution = (800, 400)) +fig = Figure(size = (800, 400)) ax1 = LScene(fig[1, 1], show_axis=false) -p1 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :red, shading = false, transparency = true) -p2 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :blue, shading = false, transparency = true) -p3 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :red, shading = false, transparency = true) +p1 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :red, shading = NoShading, transparency = true) +p2 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :blue, shading = NoShading, transparency = true) +p3 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :red, shading = NoShading, transparency = true) for (dz, p) in zip((-1, 0, 1), (p1, p2, p3)) translate!(p, 0, 0, dz) end ax2 = LScene(fig[1, 2], show_axis=false) -p1 = mesh!(ax2, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = false, transparency=true) -p2 = mesh!(ax2, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = false, transparency=true) +p1 = mesh!(ax2, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = NoShading, transparency=true) +p2 = mesh!(ax2, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = NoShading, transparency=true) rotate!(p1, Vec3f(0, 1, 0), 0.1) rotate!(p2, Vec3f(0, 1, 0), -0.1) fig diff --git a/docs/misc/banner/banner.jl b/docs/misc/banner/banner.jl index f2c9e9bb6b5..e67d6c9dd91 100644 --- a/docs/misc/banner/banner.jl +++ b/docs/misc/banner/banner.jl @@ -6,7 +6,7 @@ GLMakie.activate!() function copy_scene_settings(s) cc = cameracontrols(s) ( - px_area = s.px_area[], + viewport = s.viewport[], eyeposition = cc.eyeposition[], lookat = cc.lookat[], upvector = cc.upvector[], @@ -30,7 +30,7 @@ function apply_camera_settings!(s, settings) cc.pulser[] = settings.pulser Makie.update!(s) Makie.update!(s) - resize!(s, settings.px_area) + resize!(s, settings.viewport) return end diff --git a/docs/misc/mandelbrot/mandelbrot_video.jl b/docs/misc/mandelbrot/mandelbrot_video.jl index 978760a8ae0..b909d7aa75a 100644 --- a/docs/misc/mandelbrot/mandelbrot_video.jl +++ b/docs/misc/mandelbrot/mandelbrot_video.jl @@ -7,7 +7,7 @@ x = Observable(range(-2, 1, length = 400)) y = Observable(range(-1, 1, length = 300)) fig, ax, img = heatmap(x, y, mandelbrot, colormap = Reverse(:deep), - figure = (resolution = (400, 300),)) + figure = (size = (400, 300),)) hidedecorations!(ax) record(fig, "mandelbrot.mp4", 1:200) do frame diff --git a/docs/reference/blocks/axis.md b/docs/reference/blocks/axis.md index b59f91d4647..bc7eaf05a71 100644 --- a/docs/reference/blocks/axis.md +++ b/docs/reference/blocks/axis.md @@ -322,4 +322,4 @@ For those purposes, you can overload the methods `registration_setup!(parent, in ## Attributes -\attrdocs{Axis} \ No newline at end of file +\attrdocs{Axis} diff --git a/docs/reference/blocks/axis3.md b/docs/reference/blocks/axis3.md index d11ea8c5cd5..5b478575934 100644 --- a/docs/reference/blocks/axis3.md +++ b/docs/reference/blocks/axis3.md @@ -2,4 +2,4 @@ ## Attributes -\attrdocs{Axis3} \ No newline at end of file +\attrdocs{Axis3} diff --git a/docs/reference/blocks/gridlayout.md b/docs/reference/blocks/gridlayout.md index a0eeaba8519..2efebae2f19 100644 --- a/docs/reference/blocks/gridlayout.md +++ b/docs/reference/blocks/gridlayout.md @@ -158,7 +158,7 @@ grid with Inside alignment, and they are both effectively aligned exactly the sa ```julia using CairoMakie -f = Figure(resolution = (800, 800)) +f = Figure(size = (800, 800)) Axis(f[1, 1], title = "No grid layout") Axis(f[2, 1], title = "No grid layout") @@ -206,7 +206,7 @@ side titles. ```julia using CairoMakie -f = Figure(resolution = (800, 800)) +f = Figure(size = (800, 800)) Axis(f[1, 1]) for i in 1:3 @@ -233,7 +233,7 @@ This behavior has changed since GridLayoutBase.jl `v0.7.0`. ```julia using CairoMakie -f = Figure(resolution = (800, 800)) +f = Figure(size = (800, 800)) for i in 1:3, j in 1:3 Axis(f[i, j]) diff --git a/docs/reference/blocks/intervalslider.md b/docs/reference/blocks/intervalslider.md index 9d69babf4f7..964c547fe71 100644 --- a/docs/reference/blocks/intervalslider.md +++ b/docs/reference/blocks/intervalslider.md @@ -51,7 +51,7 @@ colors = lift(rs_h.interval, rs_v.interval) do h_int, v_int end end -scatter!(points, color = colors, colormap = [:black, :orange], strokewidth = 0) +scatter!(points, color = colors, colormap = [:gray90, :dodgerblue], strokewidth = 0) f ``` diff --git a/docs/reference/blocks/label.md b/docs/reference/blocks/label.md index 3960ad602fe..fe8451a64c0 100644 --- a/docs/reference/blocks/label.md +++ b/docs/reference/blocks/label.md @@ -19,7 +19,7 @@ fig[1:2, 1:3] = [Axis(fig) for _ in 1:6] supertitle = Label(fig[0, :], "Six plots", fontsize = 30) -sideinfo = Label(fig[2:3, 0], "This text is vertical", rotation = pi/2) +sideinfo = Label(fig[1:2, 0], "This text is vertical", rotation = pi/2) fig ``` @@ -35,19 +35,21 @@ CairoMakie.activate!() # hide f = Figure() Label(f[1, 1], - "Left Justified\nMultiline\nLabel\nLineheight 0.9", + "Multiline label\nwith\njustification = :left\nand\nlineheight = 0.9", justification = :left, lineheight = 0.9 ) Label(f[1, 2], - "Center Justified\nMultiline\nLabel\nLineheight 1.1", + "Multiline label\nwith\njustification = :center\nand\nlineheight = 1.1", justification = :center, - lineheight = 1.1 + lineheight = 1.1, + color = :dodgerblue, ) Label(f[1, 3], - "Right Justified\nMultiline\nLabel\nLineheight 1.3", + "Multiline label\nwith\njustification = :right\nand\nlineheight = 1.3", justification = :right, - lineheight = 1.3 + lineheight = 1.3, + color = :firebrick ) f diff --git a/docs/reference/blocks/legend.md b/docs/reference/blocks/legend.md index 1d4e5b3ba0f..ac606452b54 100644 --- a/docs/reference/blocks/legend.md +++ b/docs/reference/blocks/legend.md @@ -279,15 +279,9 @@ using CairoMakie f = Figure() -Axis(f[1, 1]) - markersizes = [5, 10, 15, 20] colors = [:red, :green, :blue, :orange] -for ms in markersizes, color in colors - scatter!(randn(5, 2), markersize = ms, color = color) -end - group_size = [MarkerElement(marker = :circle, color = :black, strokecolor = :transparent, markersize = ms) for ms in markersizes] @@ -298,23 +292,28 @@ group_color = [PolyElement(color = color, strokecolor = :transparent) legends = [Legend(f, [group_size, group_color], [string.(markersizes), string.(colors)], - ["Size", "Color"]) for _ in 1:6] + ["Size", "Color"], tellheight = true) for _ in 1:4] -f[1, 2:4] = legends[1:3] -f[2:4, 2] = legends[4:6] +f[1, 1:2] = legends[1:2] +f[2, :] = legends[3] +f[3, :] = legends[4] -for l in legends[4:6] +for l in legends[3:4] l.orientation = :horizontal l.tellheight = true l.tellwidth = false end legends[2].titleposition = :left -legends[5].titleposition = :left +legends[4].titleposition = :left + +legends[1].nbanks = 2 +legends[4].nbanks = 2 -legends[3].nbanks = 2 -legends[5].nbanks = 2 -legends[6].nbanks = 2 +Label(f[1, 1, Left()], "titleposition = :top\norientation = :vertical\nnbanks = 2", font = :italic, padding = (0, 10, 0, 0)) +Label(f[1, 2, Right()], "titleposition = :left\norientation = :vertical\nnbanks = 1", font = :italic, padding = (10, 0, 0, 0)) +Label(f[2, 1:2, Top()], "titleposition = :top, orientation = :horizontal\nnbanks = 1", font = :italic) +Label(f[3, 1:2, Top()], "titleposition = :left, orientation = :horizontal\nnbanks = 2", font = :italic) f ``` @@ -322,4 +321,4 @@ f ## Attributes -\attrdocs{Legend} \ No newline at end of file +\attrdocs{Legend} diff --git a/docs/reference/blocks/polaraxis.md b/docs/reference/blocks/polaraxis.md index 1ca6d919833..eede5ff6422 100644 --- a/docs/reference/blocks/polaraxis.md +++ b/docs/reference/blocks/polaraxis.md @@ -28,7 +28,7 @@ The order of a arguments can be changed with `ax.theta_as_x`. \begin{examplefigure}{svg = true} ```julia -f = Figure(resolution = (800, 400)) +f = Figure(size = (800, 400)) ax = PolarAxis(f[1, 1], title = "Theta as x") lineobject = lines!(ax, 0..2pi, sin, color = :red) @@ -48,7 +48,7 @@ For example, we can limit `thetalimits` to a smaller range to generate a circle \begin{examplefigure}{svg = true} ```julia -f = Figure(resolution = (600, 600)) +f = Figure(size = (600, 600)) ax = PolarAxis(f[1, 1], title = "Default") lines!(ax, range(0, 8pi, length=300), range(0, 10, length=300)) @@ -98,13 +98,13 @@ As a replacement for `heatmap` you can use `voronoiplot`, which generates cells \begin{examplefigure}{svg = false} ```julia -f = Figure(resolution = (800, 500)) +f = Figure(size = (800, 500)) ax = PolarAxis(f[1, 1], title = "Surface") rs = 0:10 phis = range(0, 2pi, 37) cs = [r+cos(4phi) for phi in phis, r in rs] -p = surface!(ax, 0..2pi, 0..10, cs, shading = false, colormap = :coolwarm) +p = surface!(ax, 0..2pi, 0..10, cs, shading = NoShading, colormap = :coolwarm) ax.gridz[] = 100 tightlimits!(ax) # surface plots include padding by default Colorbar(f[2, 1], p, vertical = false, flipaxis = false) @@ -131,7 +131,7 @@ axis spine. You can manipulate it with the `spine...` attributes. \begin{examplefigure}{svg = true} ```julia -f = Figure(resolution = (800, 400)) +f = Figure(size = (800, 400)) ax1 = PolarAxis(f[1, 1], title = "No spine", spinevisible = false) scatterlines!(ax1, range(0, 1, length=100), range(0, 10pi, length=100), color = 1:100) @@ -150,7 +150,7 @@ attributes in much the same way. \begin{examplefigure}{svg = true} ```julia -f = Figure(resolution = (600, 600), backgroundcolor = :black) +f = Figure(size = (600, 600), backgroundcolor = :black) ax = PolarAxis( f[1, 1], backgroundcolor = :black, diff --git a/docs/reference/plots/arrows.md b/docs/reference/plots/arrows.md index 041dceaa2e6..54cef0bb62b 100644 --- a/docs/reference/plots/arrows.md +++ b/docs/reference/plots/arrows.md @@ -42,7 +42,7 @@ using CairoMakie CairoMakie.activate!() # hide -f = Figure(resolution = (800, 800)) +f = Figure(size = (800, 800)) Axis(f[1, 1], backgroundcolor = "black") xs = LinRange(0, 2pi, 20) @@ -103,7 +103,7 @@ using CairoMakie CairoMakie.activate!() # hide -fig = Figure(resolution = (800, 800)) +fig = Figure(size = (800, 800)) ax = Axis(fig[1, 1], backgroundcolor = "black") xs = LinRange(0, 2pi, 20) ys = LinRange(0, 3pi, 20) diff --git a/docs/reference/plots/contourf.md b/docs/reference/plots/contourf.md index bf8539a0b49..27e94fb28d7 100644 --- a/docs/reference/plots/contourf.md +++ b/docs/reference/plots/contourf.md @@ -85,7 +85,7 @@ CairoMakie.activate!() # hide volcano = readdlm(Makie.assetpath("volcano.csv"), ',', Float64) -f = Figure(resolution = (800, 400)) +f = Figure(size = (800, 400)) Axis(f[1, 1], title = "Relative mode, drop lowest 30%") contourf!(volcano, levels = 0.3:0.1:1, mode = :relative) diff --git a/docs/reference/plots/datashader.md b/docs/reference/plots/datashader.md index b3086fa15f2..0f5e3fa588a 100644 --- a/docs/reference/plots/datashader.md +++ b/docs/reference/plots/datashader.md @@ -21,7 +21,7 @@ fig, ax, ds = datashader(airports, # for documentation output we shouldn't calculate the image async, # since it won't wait for the render to finish and inline a blank image async = false, - figure = (; figurepadding=0, resolution=(360*3, 160*3)) + figure = (; figurepadding=0, size=(360*3, 160*3)) ) Colorbar(fig[1, 2], ds, label="Number of airports") hidedecorations!(ax); hidespines!(ax) @@ -60,7 +60,7 @@ cargs = [[0, 0, -1.3, -1.3, -1.8, -1.9], [0, 0, -1.7, 1.5, -0.5, 0.7] ] -fig = Figure(resolution=(1000, 1000)) +fig = Figure(size=(1000, 1000)) fig_grid = CartesianIndices((3, 4)) cmap = to_colormap(:BuPu_9) cmap[1] = RGBAf(1, 1, 1, 1) # make sure background is white @@ -120,7 +120,7 @@ end f, ax, dsplot = datashader(points; colormap=:fire, axis=(; type=Axis, autolimitaspect = 1), - figure=(;figure_padding=0, resolution=(1200, 600)) + figure=(;figure_padding=0, size=(1200, 600)) ) # make image fill the whole screen hidedecorations!(ax) @@ -158,7 +158,7 @@ points = Mmap.mmap(open(path, "r"), Vector{Point2f}); =# point_transform=reverse, axis=(; type=Axis, autolimitaspect = 1), - figure=(;figure_padding=0, resolution=(1200, 600)) + figure=(;figure_padding=0, size=(1200, 600)) ) hidedecorations!(ax) hidespines!(ax) @@ -206,7 +206,7 @@ fig We can also re-use the previous NYC example for a categorical plot: ```julia @time begin - f = Figure(figure_padding=0, resolution=(1200, 600)) + f = Figure(figure_padding=0, size=(1200, 600)) ax = Axis( f[1, 1], autolimitaspect=1, diff --git a/docs/reference/plots/hexbin.md b/docs/reference/plots/hexbin.md index 2c2452d1ed0..afe042dd7ad 100644 --- a/docs/reference/plots/hexbin.md +++ b/docs/reference/plots/hexbin.md @@ -17,7 +17,7 @@ CairoMakie.activate!() # hide using Random Random.seed!(1234) -f = Figure(resolution = (800, 800)) +f = Figure(size = (800, 800)) x = rand(300) y = rand(300) @@ -43,7 +43,7 @@ CairoMakie.activate!() # hide using Random Random.seed!(1234) -f = Figure(resolution = (800, 800)) +f = Figure(size = (800, 800)) x = rand(300) y = rand(300) @@ -75,7 +75,7 @@ CairoMakie.activate!() # hide using Random Random.seed!(1234) -f = Figure(resolution = (800, 800)) +f = Figure(size = (800, 800)) x = rand(300) y = rand(300) @@ -103,7 +103,7 @@ CairoMakie.activate!() # hide using Random Random.seed!(1234) -f = Figure(resolution = (800, 800)) +f = Figure(size = (800, 800)) x = rand(300) y = rand(300) @@ -131,7 +131,7 @@ CairoMakie.activate!() # hide using Random Random.seed!(1234) -f = Figure(resolution = (800, 800)) +f = Figure(size = (800, 800)) x = randn(100000) y = randn(100000) @@ -216,7 +216,7 @@ CairoMakie.activate!() # hide using Random Random.seed!(1234) -f = Figure(resolution = (800, 800)) +f = Figure(size = (800, 800)) x = 1:100 y = 1:100 diff --git a/docs/reference/plots/mesh.md b/docs/reference/plots/mesh.md index 9f7941e0cc4..2f9f06d64cd 100644 --- a/docs/reference/plots/mesh.md +++ b/docs/reference/plots/mesh.md @@ -24,7 +24,7 @@ faces = [ colors = [:red, :green, :blue, :orange] -scene = mesh(vertices, faces, color = colors, shading = false) +scene = mesh(vertices, faces, color = colors, shading = NoShading) ``` \end{examplefigure} @@ -41,7 +41,7 @@ mesh( brain, color = [tri[1][2] for tri in brain for i in 1:3], colormap = Reverse(:Spectral), - figure = (resolution = (1000, 1000),) + figure = (size = (1000, 1000),) ) ``` \end{examplefigure} diff --git a/docs/reference/plots/rainclouds.md b/docs/reference/plots/rainclouds.md index 13756770aa7..2a5f434bcee 100644 --- a/docs/reference/plots/rainclouds.md +++ b/docs/reference/plots/rainclouds.md @@ -142,7 +142,7 @@ With and Without Box Plot \begin{examplefigure}{} ```julia -fig = Figure(resolution = (800*2, 600*5)) +fig = Figure(size = (800*2, 600*5)) colors = [Makie.wong_colors(); Makie.wong_colors()] category_labels, data_array = mockup_categories_and_data_array(3) diff --git a/docs/reference/plots/surface.md b/docs/reference/plots/surface.md index ffaa7f07926..04a702c0e99 100644 --- a/docs/reference/plots/surface.md +++ b/docs/reference/plots/surface.md @@ -79,7 +79,7 @@ data = 0.1randn(d,d) + reshape( d, d ) -surface(data; shading=false, colormap = :deep) -surface(data; shading=false, colormap = :deep) +surface(data; shading = NoShading, colormap = :deep) +surface(data; shading = NoShading, colormap = :deep) ``` \end{examplefigure} diff --git a/docs/reference/plots/text.md b/docs/reference/plots/text.md index 582e4466f67..103c497002c 100644 --- a/docs/reference/plots/text.md +++ b/docs/reference/plots/text.md @@ -99,7 +99,7 @@ using CairoMakie CairoMakie.activate!() # hide -scene = Scene(camera = campixel!, resolution = (800, 800)) +scene = Scene(camera = campixel!, size = (800, 800)) points = [Point(x, y) .* 200 for x in 1:3 for y in 1:3] scatter!(scene, points, marker = :circle, markersize = 10px) diff --git a/docs/reference/plots/volume.md b/docs/reference/plots/volume.md index e0d67f28f40..e2c7ff51464 100644 --- a/docs/reference/plots/volume.md +++ b/docs/reference/plots/volume.md @@ -28,7 +28,7 @@ brain = niread(Makie.assetpath("brain.nii.gz")).raw mini, maxi = extrema(brain) normed = Float32.((brain .- mini) ./ (maxi - mini)) -fig = Figure(resolution=(1000, 450)) +fig = Figure(size=(1000, 450)) # Make a colormap, with the first value being transparent colormap = to_colormap(:plasma) colormap[1] = RGBAf(0,0,0,0) diff --git a/docs/reference/plots/voronoiplot.md b/docs/reference/plots/voronoiplot.md index cf6cbbfb36d..f957a2973bb 100644 --- a/docs/reference/plots/voronoiplot.md +++ b/docs/reference/plots/voronoiplot.md @@ -17,7 +17,7 @@ using Random Random.seed!(1234) -f = Figure(resolution=(1200, 450)) +f = Figure(size=(1200, 450)) ax = Axis(f[1, 1]) voronoiplot!(ax, rand(Point2f, 50)) diff --git a/docs/reference/plotting-2.7-billion-points.jl b/docs/reference/plotting-2.7-billion-points.jl index d4f72794dc9..c9ae61305c1 100644 --- a/docs/reference/plotting-2.7-billion-points.jl +++ b/docs/reference/plotting-2.7-billion-points.jl @@ -61,7 +61,7 @@ points = Mmap.mmap(open(path, "r"), Vector{Point2f}); =# point_func=reverse, axis=(; type=Axis, autolimitaspect=1), - figure=(; figure_padding=0, resolution=(1200, 600))) + figure=(; figure_padding=0, size=(1200, 600))) hidedecorations!(ax) hidespines!(ax) display(f) diff --git a/docs/reference/scene.md b/docs/reference/scene.md new file mode 100644 index 00000000000..fe7252d7605 --- /dev/null +++ b/docs/reference/scene.md @@ -0,0 +1,5 @@ +# Scene + +{{list_folder_with_images scene}} + +See also: [Scene page](https://docs.makie.org/stable/explanations/scenes/) \ No newline at end of file diff --git a/docs/reference/scene/SSAO.md b/docs/reference/scene/SSAO.md new file mode 100644 index 00000000000..653789af1a5 --- /dev/null +++ b/docs/reference/scene/SSAO.md @@ -0,0 +1,43 @@ +# SSAO + +GLMakie also implements [_screen-space ambient occlusion_](https://learnopengl.com/Advanced-Lighting/SSAO), which is an algorithm to more accurately simulate the scattering of light. There are a couple of controllable scene attributes nested within the `SSAO` toplevel attribute: + +- `radius` sets the range of SSAO. You may want to scale this up or + down depending on the limits of your coordinate system +- `bias` sets the minimum difference in depth required for a pixel to + be occluded. Increasing this will typically make the occlusion + effect stronger. +- `blur` sets the (pixel) range of the blur applied to the occlusion texture. + The texture contains a (random) pattern, which is washed out by + blurring. Small `blur` will be faster, sharper and more patterned. + Large `blur` will be slower and smoother. Typically `blur = 2` is + a good compromise. + +!!! note + The SSAO postprocessor is turned off by default to save on resources. To turn it on, set `GLMakie.activate!(ssao=true)`, close any existing GLMakie window and reopen it. + +## Example + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!(ssao=true) +GLMakie.closeall() # close any open screen + +fig = Figure() +ssao = Makie.SSAO(radius = 5.0, blur = 3) +ax = LScene(fig[1, 1], scenekw = (ssao=ssao,)) +# SSAO attributes are per scene +ax.scene.ssao.bias[] = 0.025 + +box = Rect3(Point3f(-0.5), Vec3f(1)) +positions = [Point3f(x, y, rand()) for x in -5:5 for y in -5:5] +meshscatter!(ax, positions, marker=box, markersize=1, color=:lightblue, ssao=true) +fig +``` +\end{examplefigure} + +```julia:disable-ssao +GLMakie.activate!(ssao=false) # hide +GLMakie.closeall() # hide +``` diff --git a/docs/reference/scene/lighting.md b/docs/reference/scene/lighting.md new file mode 100644 index 00000000000..d32cb522c37 --- /dev/null +++ b/docs/reference/scene/lighting.md @@ -0,0 +1,280 @@ +# Lighting + +The Lighting capabilities of Makie differ between backends and plot types. +They are implemented for mesh related plot types (`mesh`, `meshscatter`, `surface`), their derivatives (e.g. 3D `arrows`) and to some degree `volume` plots (and `contour3d`). +With respect to Backends: + +- GLMakie implements the baseline lighting model and will act as our default for this page. +- WGLMakie implements a simplified version of GLMakie's lighting. +- CairoMakie implements limited lighting due to its limited 3D capabilities +- RPRMakie implements parts of Makies lighting model but can also use more sophisticated methods from RadeonProRender. + +## Material Attributes + +In 3D rendering a material describes how an object reacts to light. +This can include the color of an object, how bright and sharp specular reflections are, how metallic it looks, how rough it is and more. +In Makie however the model is still fairly simple and limited. +Currently the following material attributes are available: +- `diffuse::Vec3f = Vec3f(1.0)`: controls how strong the diffuse reflections of an object are in the red, green and blue color channel. A diffuse reflection is one where incoming light is scattered in every direction. The strength of this reflection is based on the amount of light hitting the surface, which is proportional to `dot(light_direction, -normal)`. It generally makes up the main color of an object in light. +- `specular::Vec3f = Vec3f(0.4)`: controls the strength of specular reflection in the red, green and blue color channels. A specular reflection is a direct reflection of light, i.e. one where the incoming angle `dot(light_direction, -normal)` matches the outgoing angle `dot(camera_direction, -normal)`. It responsible for bright spots on objects. Note that this does not take the color of the object into account, as specular reflections typically match the light color. +- `shininess::Float32 = 32f0`: controls how sharp specular reflections are. Low shininess will allow a larger difference between incoming outgoing angle to take effect, creating a larger and smoother bright spot. High shininess will respectively reduce the size of the bright spot and increase its sharpness. This value must be positive. +- `backlight::Real = 0` controls how strongly light interacts with the backside of an object. Setting this to a value `> 0` can be helpful when visualizing a surface. (More precisely the light calculation is repeated with inverted normals and the result is mixed in with `backlight` as a prefactor.) + +!!! note + RPRMakie does not use these material attributes. + Instead it relies on RadeonProRender's material system, which is passed through the `material` attribute. + See the [RPRMakie page](https://docs.makie.org/stable/documentation/backends/rprmakie/) for examples. + + +## Lighting alogrithm + +Lights are controlled through the `lights` vector in a `scene` and by the `shading` attribute in a plot. +Generally you will not need to set `shading` yourself, as it is derived based on the lights vector. +The possible options for `shading` are: +- `shading = NoShading` disables light calculations, resulting in the plain color of an object being shown. +- `shading = FastShading` enables a simplified lighting model which only allows for one `AmbientLight` and one `DirectionalLight`. +- `shading = MultiLightShading` is a GLMakie exclusive option which enables multiple light sources (as set in the `ScreenConfig`, default up to 64) as well as `PointLight` and `SpotLight`. + +!!! note + You can access the underlying scene of an `Axis3` with `ax.scene`. + +For reference all the lighting calculations (except ambient) in GLMakie, WGLMakie and to some extend CairoMakie end up using the [Blinn-Phong reflection model](https://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_reflection_model) which boils down to + +```julia +function blinn_phong( + diffuse, specular, shininess, normal, object_color, + light_color, light_direction, camera_direction + ) + diffuse_coefficient = max(dot(light_direction, -normal), 0.0) + H = normalize(light_direction + camera_direction) + specular_coefficient = max(dot(H, -normal), 0.0)^shininess + return light_color * ( + diffuse * diffuse_coefficient * object_color + + specular * specular_coefficient + ) +end +``` + +The different light sources control the `light_direction` and may further adjust the result of this function. For example, `SpotLight` adds a factor which reduces light intensity outside its area. + + +## Types of Light + + +### AmbientLight + +{{doc AmbientLight}} + +\begin{examplefigure}{} +```julia +using CairoMakie +CairoMakie.activate!() # hide + +fig = Figure(size = (600, 600)) +ax11 = LScene(fig[1, 1], scenekw = (lights = [],)) +ax12 = LScene(fig[1, 2], scenekw = (lights = [AmbientLight(RGBf(0, 0, 0))],)) +ax21 = LScene(fig[2, 1], scenekw = (lights = [AmbientLight(RGBf(0.7, 0.7, 0.7))],)) +ax22 = LScene(fig[2, 2], scenekw = (lights = [AmbientLight(RGBf(0.8, 0.3, 0))],)) +for ax in (ax11, ax12, ax21, ax22) + mesh!(ax, Sphere(Point3f(0), 1f0), color = :white) +end +fig +``` +\end{examplefigure} + + +### DirectionalLight + +{{doc DirectionalLight}} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +fig = Figure(size = (600, 600)) +ax11 = LScene(fig[1, 1], scenekw = (lights = [DirectionalLight(RGBf(0, 0, 0), Vec3f(-1, 0, 0))],)) +ax12 = LScene(fig[1, 2], scenekw = (lights = [DirectionalLight(RGBf(1, 1, 1), Vec3f(-1, 0, 0))],)) +lights = [ + DirectionalLight(RGBf(0, 0, 0.7), Vec3f(-1, -1, 0)), + DirectionalLight(RGBf(0.7, 0.2, 0), Vec3f(-1, 1, -1)), + DirectionalLight(RGBf(0.7, 0.7, 0.7), Vec3f(1, -1, -1)) +] +ax21 = LScene(fig[2, 1], scenekw = (lights = lights,)) +ax22 = LScene(fig[2, 2], scenekw = (lights = [DirectionalLight(RGBf(4, 2, 1), Vec3f(0, 0, -1))],)) +for ax in (ax11, ax12, ax21, ax22) + mesh!(ax, Sphere(Point3f(0), 1f0), color = :white) +end +fig +``` +\end{examplefigure} + +### PointLight + +{{doc PointLight}} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +fig = Figure(size = (600, 600)) +ax = LScene(fig[1, 1], scenekw = (lights = [PointLight(RGBf(1, 1, 1), Point3f(0, 0, 0))],)) +ps = [Point3f(x, y, z) for x in (-1, 0, 1) for y in (-1, 0, 1) for z in (-1, 0, 1)] +meshscatter!(ax, ps, color = :white) +fig +``` +\end{examplefigure} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +lights = [ + PointLight(RGBf(1, 1, 1), Point3f(0, 0, 5), 50), + PointLight(RGBf(2, 0, 0), Point3f(-3, -3, 2), 10), + PointLight(RGBf(0, 2, 0), Point3f(-3, 3, 2), 10), + PointLight(RGBf(0, 0, 2), Point3f( 3, 3, 2), 10), + PointLight(RGBf(2, 2, 0), Point3f( 3, -3, 2), 10), +] + +fig = Figure(size = (600, 600)) +ax = LScene(fig[1, 1], scenekw = (lights = lights,)) +ps = [Point3f(x, y, 0) for x in -5:5 for y in -5:5] +meshscatter!(ax, ps, color = :white, markersize = 0.75) +scatter!(ax, map(l -> l.position[], lights), color = map(l -> l.color[], lights), strokewidth = 1, strokecolor = :black) +fig +``` +\end{examplefigure} + +With a strong PointLight and Attenuation you can create different colors at different distances. + +\begin{examplefigure}{} +```julia +using GLMakie, GeometryBasics +GLMakie.activate!() # hide + +ps = [ + Point3f(cosd(phi) * cosd(theta), sind(phi) * cosd(theta), sind(theta)) + for theta in range(-20, 20, length = 21) for phi in range(60, 340, length=30) +] +faces = [QuadFace(30j + i, 30j + mod1(i+1, 30), 30*(j+1) + mod1(i+1, 30), 30*(j+1) + i) for j in 0:19 for i in 1:29] +m = GeometryBasics.Mesh(meta(ps, normals = ps), decompose(GLTriangleFace, faces)) + +lights = [PointLight(RGBf(10, 4, 2), Point3f(0, 0, 0), 5)] + +fig = Figure(size = (600, 600), backgroundcolor = :black) +ax = LScene(fig[1, 1], scenekw = (lights = lights,), show_axis = false) +update_cam!(ax.scene, ax.scene.camera_controls, Rect3f(Point3f(-2), Vec3f(4))) +meshscatter!( + ax, [Point3f(0) for _ in 1:14], marker = m, markersize = 0.1:0.2:3.0, + color = :white, backlight = 1, transparency = false) +fig +``` +\end{examplefigure} + + +### SpotLight + +{{doc SpotLight}} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide +GLMakie.closeall() # hide + +lights = [ + SpotLight(RGBf(1, 0, 0), Point3f(-3, 0, 3), Vec3f(0, 0, -1), Vec2f(0.0, 0.3pi)), + SpotLight(RGBf(0, 1, 0), Point3f( 0, 3, 3), Vec3f(0, -0.5, -1), Vec2f(0.2pi, 0.25pi)), + SpotLight(RGBf(0, 0, 1), Point3f( 3, 0, 3), Vec3f(0, 0, -1), Vec2f(0.25pi, 0.25pi)), +] + +fig = Figure(size = (600, 600)) +ax = LScene(fig[1, 1], scenekw = (lights = lights,)) +ps = [Point3f(x, y, 0) for x in -5:5 for y in -5:5] +meshscatter!(ax, ps, color = :white, markersize = 0.75) +scatter!(ax, map(l -> l.position[], lights), color = map(l -> l.color[], lights), strokewidth = 1, strokecolor = :black) +fig +``` +\end{examplefigure} + +### RectLight + +{{doc RectLight}} + +\begin{examplefigure}{} +```julia +using FileIO, GeometryBasics, LinearAlgebra, GLMakie + +# Create mesh from RectLight parameters +function to_mesh(l::RectLight) + n = -normalize(cross(l.u1[], l.u2[])) + p = l.position[] - 0.5 * l.u1[] - 0.5 * l.u2[] + positions = [p, p + l.u1[], p + l.u2[], p + l.u1[] + l.u2[]] + faces = GLTriangleFace[(1,2,3), (2,3,4)] + normals = [n,n,n,n] + return GeometryBasics.Mesh(meta(positions, normals = normals), faces) +end + +fig = Figure(backgroundcolor = :black) + +# Prepare lights +lights = Makie.AbstractLight[ + AmbientLight(RGBf(0.1, 0.1, 0.1)), + RectLight(RGBf(0.9, 1, 0.8), Rect2f(-1.9, -1.9, 1.8, 1.8)), + RectLight(RGBf(0.9, 1, 0.8), Rect2f(-1.9, 0.1, 1.8, 1.8)), + RectLight(RGBf(0.9, 1, 0.8), Rect2f( 0.1, 0.1, 1.8, 1.8)), + RectLight(RGBf(0.9, 1, 0.8), Rect2f( 0.1, -1.9, 1.8, 1.8)), +] + +for l in lights + if l isa RectLight + angle = pi/4 + p = l.position[] + Makie.rotate!(l, Vec3f(0, 1, 0), angle) + + p = 3 * Vec3f(1+sin(angle), 0, cos(angle)) + + p[1] * normalize(l.u1[]) + + p[2] * normalize(l.u2[]) + translate!(l, p) + end +end + +# Set scene +scene = LScene( + fig[1, 1], show_axis = false, + scenekw=(lights = lights, backgroundcolor = :black, center = false), +) + +# floor +p = mesh!(scene, Rect3f(Point3f(-10, -10, 0.01), Vec3f(20, 20, 0.02)), color = :white) +translate!(p, 0, 0, -5) + +# Cat +cat_mesh = FileIO.load(Makie.assetpath("cat.obj")) +cat_texture = FileIO.load(Makie.assetpath("diffusemap.png")) +p2 = mesh!(scene, cat_mesh, color = cat_texture) +Makie.rotate!(p2, Vec3f(1,0,0), pi/2) +translate!(p2, -2, 2, -5) +scale!(p2, Vec3f(4)) + +# Window/light source markers +for l in lights + if l isa RectLight + m = to_mesh(l) + mesh!(m, color = :white, backlight = 1) + end +end + +# place camera +update_cam!(scene.scene, Vec3f(1.5, -13, 2), Vec3f(1, -2, 0), Vec3f(0, 0, 1)) + +fig +``` +\end{examplefigure} + +### EnvironmentLight + +{{doc EnvironmentLight}} diff --git a/docs/reference/scene/matcap.md b/docs/reference/scene/matcap.md new file mode 100644 index 00000000000..a9b320dc29b --- /dev/null +++ b/docs/reference/scene/matcap.md @@ -0,0 +1,17 @@ +# Matcap + +A matcap (material capture) is a texture which is applied based on the normals of a given mesh. They typically include complex materials and lighting and offer a cheap way to apply those to any mesh. You may pass a matcap via the `matcap` attribute of a `mesh`, `meshscatter` or `surface` plot. Setting `shading = NoShading` is suggested. You can find a lot matcaps [here](https://github.com/nidorx/matcaps). + +## Example + +\begin{examplefigure}{} +```julia +using FileIO +using GLMakie +GLMakie.activate!() # hide +catmesh = FileIO.load(assetpath("cat.obj")) +gold = FileIO.load(download("https://raw.githubusercontent.com/nidorx/matcaps/master/1024/E6BF3C_5A4719_977726_FCFC82.png")) + +mesh(catmesh, matcap=gold, shading = NoShading) +``` +\end{examplefigure} \ No newline at end of file diff --git a/docs/tutorials/aspect-tutorial.md b/docs/tutorials/aspect-tutorial.md index b0e2f2c1a1a..9249e97daa1 100644 --- a/docs/tutorials/aspect-tutorial.md +++ b/docs/tutorials/aspect-tutorial.md @@ -16,7 +16,7 @@ CairoMakie.activate!() # hide set_theme!(backgroundcolor = :gray90) -f = Figure(resolution = (800, 500)) +f = Figure(size = (800, 500)) ax = Axis(f[1, 1], aspect = 1) Colorbar(f[1, 2]) f @@ -61,7 +61,7 @@ Let's try the example from above again, but this time we force the column of the \begin{examplefigure}{svg = true} ```julia -f = Figure(resolution = (800, 500)) +f = Figure(size = (800, 500)) ax = Axis(f[1, 1]) Colorbar(f[1, 2]) colsize!(f.layout, 1, Aspect(1, 1.0)) @@ -113,7 +113,7 @@ Let's return to our previous state with a square axis: \begin{examplefigure}{svg = true} ```julia # hide -f = Figure(resolution = (800, 500)) +f = Figure(size = (800, 500)) ax = Axis(f[1, 1]) Colorbar(f[1, 2]) colsize!(f.layout, 1, Aspect(1, 1.0)) diff --git a/docs/tutorials/basic-tutorial.md b/docs/tutorials/basic-tutorial.md index e3c5b65f302..c933635e7e9 100644 --- a/docs/tutorials/basic-tutorial.md +++ b/docs/tutorials/basic-tutorial.md @@ -55,12 +55,12 @@ f = Figure(backgroundcolor = :tomato) ``` \end{examplefigure} -Another common thing to do is to give a figure a different size or resolution. +Another common thing to do is to give a figure a different size. The default is 800x600, let's try halving the height: \begin{examplefigure}{svg = true} ```julia -f = Figure(backgroundcolor = :tomato, resolution = (800, 300)) +f = Figure(backgroundcolor = :tomato, size = (800, 300)) ``` \end{examplefigure} @@ -178,13 +178,13 @@ You can pass any kind of object with symbol-value pairs and these will be used a x = range(0, 10, length=100) y = sin.(x) scatter(x, y; - figure = (; resolution = (400, 400)), + figure = (; size = (400, 400)), axis = (; title = "Scatter plot", xlabel = "x label") ) ``` \end{examplefigure} -The `;` in `(; resolution = (400, 400))` is nothing special, it just clarifies that we want a one-element `NamedTuple` and not a variable called `resolution`. +The `;` in `(; size = (400, 400))` is nothing special, it just clarifies that we want a one-element `NamedTuple` and not a variable called `size`. It's good habit to include it but it's not needed for `NamedTuple`s with more than one entry. ## Argument conversions @@ -221,7 +221,7 @@ lines([Point(0, 0), Point(5, 10), Point(10, 5)]) The input arguments you can use with `lines` and `scatter` are mostly the same because they have the same conversion trait `PointBased`. Other plotting functions have different conversion traits, \myreflink{heatmap} for example expects two-dimensional grid data. -The respective trait is called `DiscreteSurface`. +The respective trait is called `CellGrid`. ## Layering multiple plots diff --git a/docs/tutorials/layout-tutorial.md b/docs/tutorials/layout-tutorial.md index c968e804212..6e4592ffd7d 100644 --- a/docs/tutorials/layout-tutorial.md +++ b/docs/tutorials/layout-tutorial.md @@ -17,7 +17,7 @@ using Makie.FileIO CairoMakie.activate!() # hide f = Figure(backgroundcolor = RGBf(0.98, 0.98, 0.98), - resolution = (1000, 700)) + size = (1000, 700)) ga = f[1, 1] = GridLayout() gb = f[2, 1] = GridLayout() gcd = f[1:2, 2] = GridLayout() @@ -170,7 +170,7 @@ using FileIO CairoMakie.activate!() # hide f = Figure(backgroundcolor = RGBf(0.98, 0.98, 0.98), - resolution = (1000, 700)) + size = (1000, 700)) ``` \end{examplefigure} diff --git a/docs/tutorials/scenes.md b/docs/tutorials/scenes.md index 2b5538f1094..99d4ff306f6 100644 --- a/docs/tutorials/scenes.md +++ b/docs/tutorials/scenes.md @@ -14,7 +14,7 @@ scene = Scene(; # set_theme!(lightposition=:eyeposition, ambient=RGBf(0.5, 0.5, 0.5))` lights = Makie.automatic, backgroundcolor = :gray, - resolution = (500, 500); + size = (500, 500); # gets filled in with the currently set global theme theme_kw... ) @@ -36,7 +36,7 @@ With scenes, one can create subwindows. The window extends are given by a `Rect{ using GLMakie, Makie GLMakie.activate!() scene = Scene(backgroundcolor=:gray) -subwindow = Scene(scene, px_area=Rect(100, 100, 200, 200), clear=true, backgroundcolor=:white) +subwindow = Scene(scene, viewport=Rect(100, 100, 200, 200), clear=true, backgroundcolor=:white) scene ``` \end{examplefigure} @@ -128,7 +128,7 @@ We can use those events to e.g. move the subwindow. If you execute the below in ```julia on(scene.events.mouseposition) do mousepos if ispressed(subwindow, Mouse.left & Keyboard.left_control) - subwindow.px_area[] = Rect(Int.(mousepos)..., 200, 200) + subwindow.viewport[] = Rect(Int.(mousepos)..., 200, 200) end end ``` @@ -249,12 +249,14 @@ The scene graph can be used to create rigid transformations, like for a robot ar ```julia GLMakie.activate!() # hide parent = Scene() -cam3d!(parent) +cam3d!(parent; clipping_mode = :static) # One can set the camera lookat and eyeposition, by getting the camera controls and using `update_cam!` camc = cameracontrols(parent) update_cam!(parent, camc, Vec3f(0, 8, 0), Vec3f(4.0, 0, 0)) - +# One may need to adjust the +# near and far clip plane when adjusting the camera manually +camc.far[] = 100f0 s1 = Scene(parent, camera=parent.camera) mesh!(s1, Rect3f(Vec3f(0, -0.1, -0.1), Vec3f(5, 0.2, 0.2))) s2 = Scene(s1, camera=parent.camera) @@ -368,7 +370,7 @@ lights = [ EnvironmentLight(1.5, rotl90(load(assetpath("sunflowers_1k.hdr"))')), PointLight(Vec3f(50, 0, 200), RGBf(radiance, radiance, radiance*1.1)), ] -s = Scene(resolution=(500, 500), lights=lights) +s = Scene(size=(500, 500), lights=lights) cam3d!(s) c = cameracontrols(s) c.near[] = 5 diff --git a/docs/utils.jl b/docs/utils.jl index 50b3d35f39b..b8523df4141 100644 --- a/docs/utils.jl +++ b/docs/utils.jl @@ -20,6 +20,7 @@ end using Makie function html_docstring(fname) + fname == :SpecApi && return "" doc = Base.doc(getfield(Makie, Symbol(fname))) body = Markdown.html(doc) diff --git a/metrics/ttfp/benchmark-ttfp.jl b/metrics/ttfp/benchmark-ttfp.jl index 9c422f774aa..74a76215495 100644 --- a/metrics/ttfp/benchmark-ttfp.jl +++ b/metrics/ttfp/benchmark-ttfp.jl @@ -8,24 +8,15 @@ macro ctime(x) end t_using = @ctime @eval using $Package -function get_colorbuffer(fig) - # We need to handle old versions of Makie - if isdefined(Makie, :CURRENT_BACKEND) # new version after display refactor - return Makie.colorbuffer(fig) # easy :) - else - Makie.inline!(false) - screen = display(fig; visible=false) - return Makie.colorbuffer(screen) - end -end - if Package === :WGLMakie import Electron WGLMakie.JSServe.use_electron_display() end +set_theme!(size=(800, 600)) + create_time = @ctime fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) -display_time = @ctime get_colorbuffer(fig) +display_time = @ctime colorbuffer(fig; px_per_unit=1) using BenchmarkTools using BenchmarkTools.JSON @@ -39,7 +30,7 @@ old = isfile(result) ? JSON.parse(read(result, String)) : [[], [], [], [], []] push!.(old[1:3], [t_using, create_time, display_time]) b1 = @benchmark fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) -b2 = @benchmark get_colorbuffer(fig) setup=(fig=scatter(1:4)) +b2 = @benchmark colorbuffer(fig; px_per_unit=1) using Statistics diff --git a/metrics/ttfp/run-benchmark.jl b/metrics/ttfp/run-benchmark.jl index 46c11b19319..f7560f36246 100644 --- a/metrics/ttfp/run-benchmark.jl +++ b/metrics/ttfp/run-benchmark.jl @@ -34,7 +34,7 @@ create_time = @ctime fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=2 display_time = @ctime Makie.colorbuffer(display(fig)) # Runtime create_time = @benchmark fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) -display_time = @benchmark Makie.colorbuffer(display(fig)) +display_time = @benchmark Makie.colorbuffer(fig) ``` | | using | create | display | create | display | @@ -87,27 +87,28 @@ function analyze(pr, master) std_p = (std(pr) + std(master)) / 2 m_pr = mean(pr) m_m = mean(master) - mean_diff = mean(m_pr) - mean(m_m) - percent = (1 - (m_m / m_pr)) * 100 + mean_diff = m_pr - m_m + speedup = m_m / m_pr p = pvalue(tt) mean_diff_str = string(round(mean_diff; digits=2), unit) - + percent_change = (speedup - 1) * 100 result = if p < 0.05 if abs(d) > 0.2 - indicator = abs(percent) < 5 ? ["faster ✓", "slower X"] : ["**faster**✅", "**slower**❌"] + indicator = abs(percent_change) < 5 ? ["faster ✓", "slower X"] : ["**faster**✅", "**slower**❌"] indicator[d < 0 ? 1 : 2] else "*invariant*" end else - if abs(percent) < 5 + if abs(percent_change) < 5 "*invariant*" else "*noisy*🤷‍♀️" end end - return @sprintf("%s%.2f%s, %s %s (%.2fd, %.2fp, %.2fstd)", percent > 0 ? "+" : "-", abs(percent), "%", mean_diff_str, result, d, p, std_p) + return @sprintf("%.2fx %s, %s (%.2fd, %.2fp, %.2fstd)", speedup, result, mean_diff_str, d, p, + std_p) end function summarize_stats(timings) @@ -153,6 +154,9 @@ function update_comment(old_comment, package_name, (pr_bench, master_bench, eval for (i, value) in enumerate(evaluation) rows[idx + 2][i + 1] = [value] end + open("benchmark.md", "w") do io + return show(io, md) + end return sprint(show, md) end @@ -171,9 +175,12 @@ function make_or_edit_comment(ctx, pr, package_name, benchmarks) end end + +using Random + function run_benchmarks(projects; n=n_samples) benchmark_file = joinpath(@__DIR__, "benchmark-ttfp.jl") - for project in repeat(projects; outer=n) + for project in shuffle!(repeat(projects; outer=n)) run(`$(Base.julia_cmd()) --startup-file=no --project=$(project) $benchmark_file $Package`) project_name = basename(project) end diff --git a/precompile/shared-precompile.jl b/precompile/shared-precompile.jl index 923eff489a3..efa7a3999cd 100644 --- a/precompile/shared-precompile.jl +++ b/precompile/shared-precompile.jl @@ -1,6 +1,5 @@ # File to run to snoop/trace all functions to compile using GeometryBasics - @compile poly(Recti(0, 0, 200, 200), strokewidth=20, strokecolor=:red, color=(:black, 0.4)) @compile scatter(0..1, rand(10), markersize=rand(10) .* 20) @@ -55,7 +54,7 @@ end @compile begin res = 200 - s = Scene(camera=campixel!, resolution=(res, res)) + s = Scene(camera=campixel!, size=(res, res)) half = res / 2 linewidth = 10 xstart = half - (half/2) diff --git a/src/Makie.jl b/src/Makie.jl index 36500312839..ef40b64c0eb 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -12,6 +12,12 @@ using .ContoursHygiene const Contours = ContoursHygiene.Contour using Base64 +# Import FilePaths for invalidations +# When loading Electron for WGLMakie, which depends on FilePaths +# It invalidates half of Makie. Simplest fix is to load it early on in Makie +# So that the bulk of Makie gets compiled after FilePaths invalidadet Base code +# +import FilePaths using LaTeXStrings using MathTeXEngine using Random @@ -72,20 +78,22 @@ using Base.Iterators: repeated, drop import Base: getindex, setindex!, push!, append!, parent, get, get!, delete!, haskey using Observables: listeners, to_value, notify -using MakieCore: SceneLike, MakieScreen, ScenePlot, AbstractScene, AbstractPlot, Transformable, Attributes, Combined, Theme, Plot +using MakieCore: SceneLike, MakieScreen, ScenePlot, AbstractScene, AbstractPlot, Transformable, Attributes, Plot, Theme, Plot using MakieCore: Arrows, Heatmap, Image, Lines, LineSegments, Mesh, MeshScatter, Poly, Scatter, Surface, Text, Volume, Wireframe -using MakieCore: ConversionTrait, NoConversion, PointBased, SurfaceLike, ContinuousSurface, DiscreteSurface, VolumeLike +using MakieCore: ConversionTrait, NoConversion, PointBased, GridBased, VertexGrid, CellGrid, ImageLike, VolumeLike using MakieCore: Key, @key_str, Automatic, automatic, @recipe using MakieCore: Pixel, px, Unit, Billboard +using MakieCore: NoShading, FastShading, MultiLightShading using MakieCore: not_implemented_for import MakieCore: plot, plot!, theme, plotfunc, plottype, merge_attributes!, calculated_attributes!, -get_attribute, plotsym, plotkey, attributes, used_attributes + get_attribute, plotsym, plotkey, attributes, used_attributes +import MakieCore: create_axis_like, create_axis_like!, figurelike_return, figurelike_return! import MakieCore: arrows, heatmap, image, lines, linesegments, mesh, meshscatter, poly, scatter, surface, text, volume import MakieCore: arrows!, heatmap!, image!, lines!, linesegments!, mesh!, meshscatter!, poly!, scatter!, surface!, text!, volume! import MakieCore: convert_arguments, convert_attribute, default_theme, conversion_trait export @L_str, @colorant_str -export ConversionTrait, NoConversion, PointBased, SurfaceLike, ContinuousSurface, DiscreteSurface, VolumeLike +export ConversionTrait, NoConversion, PointBased, GridBased, VertexGrid, CellGrid, ImageLike, VolumeLike export Pixel, px, Unit, plotkey, attributes, used_attributes export Linestyle @@ -107,6 +115,7 @@ include("interaction/liftmacro.jl") include("colorsampler.jl") include("patterns.jl") include("utilities/utilities.jl") # need Makie.AbstractPattern +include("lighting.jl") # Basic scene/plot/recipe interfaces + types include("scenes.jl") @@ -167,6 +176,10 @@ include("layouting/transformation.jl") include("layouting/data_limits.jl") include("layouting/layouting.jl") include("layouting/boundingbox.jl") + +# Declaritive SpecApi +include("specapi.jl") + # more default recipes # statistical recipes include("stats/conversions.jl") @@ -201,7 +214,7 @@ export help, help_attributes, help_arguments # Abstract/Concrete scene + plot types export AbstractScene, SceneLike, Scene, MakieScreen -export AbstractPlot, Combined, Atomic, OldAxis +export AbstractPlot, Plot, Atomic, OldAxis # Theming, working with Plots export Attributes, Theme, attributes, default_theme, theme, set_theme!, with_theme, update_theme! @@ -222,7 +235,7 @@ export xtickrotation, ytickrotation, ztickrotation export xtickrotation!, ytickrotation!, ztickrotation! # Observable/Signal related -export Observable, Observable, lift, map_once, to_value, on, onany, @lift, off, connect! +export Observable, Observable, lift, to_value, on, onany, @lift, off, connect! # utilities and macros export @recipe, @extract, @extractvalue, @key_str, @get_attribute @@ -245,11 +258,11 @@ export SceneSpace, PixelSpace, Pixel export AbstractCamera, EmptyCamera, Camera, Camera2D, Camera3D, cam2d!, cam2d export campixel!, campixel, cam3d!, cam3d_cad!, old_cam3d!, old_cam3d_cad!, cam_relative! export update_cam!, rotate_cam!, translate_cam!, zoom! -export pixelarea, plots, cameracontrols, cameracontrols!, camera, events +export viewport, plots, cameracontrols, cameracontrols!, camera, events export to_world # picking + interactive use cases + events -export mouseover, onpick, pick, Events, Keyboard, Mouse, mouse_selection, is_mouseinside +export mouseover, onpick, pick, Events, Keyboard, Mouse, is_mouseinside export ispressed, Exclusively export connect_screen export window_area, window_open, mouse_buttons, mouse_position, mouseposition_px, @@ -261,6 +274,7 @@ export Consume # Raymarching algorithms export RaymarchAlgorithm, IsoValue, Absorption, MaximumIntensityProjection, AbsorptionRGBA, IndexedAbsorptionRGBA export Billboard +export NoShading, FastShading, MultiLightShading # Reexports of # Color/Vector types convenient for 3d/2d graphics @@ -338,7 +352,7 @@ export Arrows , Heatmap , Image , Lines , LineSegments , Mesh , MeshScatte export arrows , heatmap , image , lines , linesegments , mesh , meshscatter , poly , scatter , surface , text , volume , wireframe export arrows! , heatmap! , image! , lines! , linesegments! , mesh! , meshscatter! , poly! , scatter! , surface! , text! , volume! , wireframe! -export PointLight, EnvironmentLight, AmbientLight, SSAO +export AmbientLight, PointLight, DirectionalLight, SpotLight, EnvironmentLight, RectLight, SSAO include("precompiles.jl") diff --git a/src/basic_recipes/arrows.jl b/src/basic_recipes/arrows.jl index 83a2a38f76c..c9a04553ea6 100644 --- a/src/basic_recipes/arrows.jl +++ b/src/basic_recipes/arrows.jl @@ -118,7 +118,7 @@ function plot!(arrowplot::Arrows{<: Tuple{AbstractVector{<: Point{N}}, V}}) wher arrowtail, color, linecolor, linestyle, linewidth, lengthscale, arrowhead, arrowsize, arrowcolor, quality, # passthrough - diffuse, specular, shininess, + diffuse, specular, shininess, shading, fxaa, ssao, transparency, visible, inspectable ) @@ -146,7 +146,7 @@ function plot!(arrowplot::Arrows{<: Tuple{AbstractVector{<: Point{N}}, V}}) wher # for 2D arrows, compute the correct marker rotation given the projection / scene size # for the screen-space marker if is_pixel_space(arrowplot.markerspace[]) - rotations = lift(arrowplot, scene.camera.projectionview, scene.px_area, headstart) do pv, pxa, hs + rotations = lift(arrowplot, scene.camera.projectionview, scene.viewport, headstart) do pv, pxa, hs angles = map(hs) do (start, stop) pstart = project(scene, start) pstop = project(scene, stop) @@ -210,25 +210,21 @@ function plot!(arrowplot::Arrows{<: Tuple{AbstractVector{<: Point{N}}, V}}) wher marker_tail = lift((at, q) -> arrow_tail(3, at, q), arrowplot, arrowtail, quality) meshscatter!( arrowplot, - start, rotations = directions, - marker=marker_tail, - markersize = msize, - color=line_c, colormap=colormap, colorscale=colorscale, colorrange=arrowplot.colorrange, - fxaa = fxaa_bool, ssao = ssao, - diffuse = diffuse, - specular = specular, shininess = shininess, inspectable = inspectable, - transparency = transparency, visible = visible + start, rotations = directions, markersize = msize, + marker = marker_tail, + color = line_c, colormap = colormap, colorscale = colorscale, colorrange = arrowplot.colorrange, + fxaa = fxaa_bool, ssao = ssao, shading = shading, + diffuse = diffuse, specular = specular, shininess = shininess, + inspectable = inspectable, transparency = transparency, visible = visible ) meshscatter!( arrowplot, - start, rotations = directions, - marker=marker_head, - markersize = markersize, - color=arrow_c, colormap=colormap, colorscale=colorscale, colorrange=arrowplot.colorrange, - fxaa = fxaa_bool, ssao = ssao, - diffuse = diffuse, - specular = specular, shininess = shininess, inspectable = inspectable, - transparency = transparency, visible = visible + start, rotations = directions, markersize = markersize, + marker = marker_head, + color = arrow_c, colormap = colormap, colorscale = colorscale, colorrange = arrowplot.colorrange, + fxaa = fxaa_bool, ssao = ssao, shading = shading, + diffuse = diffuse, specular = specular, shininess = shininess, + inspectable = inspectable, transparency = transparency, visible = visible ) end diff --git a/src/basic_recipes/axis.jl b/src/basic_recipes/axis.jl index f9744a8efb5..fcb4f803539 100644 --- a/src/basic_recipes/axis.jl +++ b/src/basic_recipes/axis.jl @@ -317,8 +317,8 @@ function draw_axis3d(textbuffer, linebuffer, scale, limits, ranges_labels, fonts return end -function plot!(scene::SceneLike, ::Type{<: Axis3D}, attributes::Attributes, args...) - axis = Axis3D(scene, attributes, args) +function plot!(axis::Axis3D) + scene = get_scene(axis) # Disable any non linear transform for the axis plot! axis.transformation.transform_func[] = identity textbuffer = TextBuffer(axis, Point3, transparency = true, markerspace = :data, @@ -334,12 +334,11 @@ function plot!(scene::SceneLike, ::Type{<: Axis3D}, attributes::Attributes, args getindex.(axis, (:showaxis, :showticks, :showgrid))..., titlevals..., framevals..., tvals..., axis.padding ) - map_once( + onany( draw_axis3d, Observable(textbuffer), Observable(linebuffer), scale(scene), - axis[1], axis.ticks.ranges_labels, Observable(axis.fonts), args... + axis[1], axis.ticks.ranges_labels, Observable(axis.fonts), args...; update=true ) - push!(scene, axis) return axis end diff --git a/src/basic_recipes/band.jl b/src/basic_recipes/band.jl index b0434868c1a..9a8de39b48d 100644 --- a/src/basic_recipes/band.jl +++ b/src/basic_recipes/band.jl @@ -13,7 +13,7 @@ $(ATTRIBUTES) default_theme(scene, Mesh)..., colorrange = automatic, ) - attr[:shading][] = false + attr[:shading][] = NoShading attr end diff --git a/src/basic_recipes/bracket.jl b/src/basic_recipes/bracket.jl index 34c2691bf85..c1d2dff71bb 100644 --- a/src/basic_recipes/bracket.jl +++ b/src/basic_recipes/bracket.jl @@ -55,7 +55,7 @@ function Makie.plot!(pl::Bracket) end onany(pl, points, scene.camera.projectionview, pl.model, transform_func(pl), - scene.px_area, pl.offset, pl.width, pl.orientation, realtextoffset, + scene.viewport, pl.offset, pl.width, pl.orientation, realtextoffset, pl.style) do points, _, _, _, _, offset, width, orientation, textoff, style empty!(bp[]) diff --git a/src/basic_recipes/contourf.jl b/src/basic_recipes/contourf.jl index d2e0d7a8f55..89ff2ae4da3 100644 --- a/src/basic_recipes/contourf.jl +++ b/src/basic_recipes/contourf.jl @@ -55,6 +55,9 @@ function _get_isoband_levels(levels::AbstractVector{<:Real}, mi, ma) @assert issorted(edges) edges end + +conversion_trait(::Type{<:Contourf}) = VertexGrid() + function _get_isoband_levels(::Val{:normal}, levels, values) return _get_isoband_levels(levels, extrema_nan(values)...) end @@ -64,9 +67,6 @@ function _get_isoband_levels(::Val{:relative}, levels::AbstractVector, values) return Float32.(levels .* (ma - mi) .+ mi) end -conversion_trait(::Type{<:Contourf}) = ContinuousSurface() - - function Makie.plot!(c::Contourf{<:Tuple{<:AbstractVector{<:Real}, <:AbstractVector{<:Real}, <:AbstractMatrix{<:Real}}}) xs, ys, zs = c[1:3] @@ -141,7 +141,7 @@ function Makie.plot!(c::Contourf{<:Tuple{<:AbstractVector{<:Real}, <:AbstractVec color = colors, strokewidth = 0, strokecolor = :transparent, - shading = false, + shading = NoShading, inspectable = c.inspectable, transparency = c.transparency ) diff --git a/src/basic_recipes/contours.jl b/src/basic_recipes/contours.jl index ed02afc1e69..f4f21f70dfc 100644 --- a/src/basic_recipes/contours.jl +++ b/src/basic_recipes/contours.jl @@ -110,10 +110,10 @@ function to_levels(n::Integer, cnorm) range(zmin + dz; step = dz, length = n) end -conversion_trait(::Type{<: Contour3d}) = ContinuousSurface() -conversion_trait(::Type{<: Contour}) = ContinuousSurface() -conversion_trait(::Type{<: Contour{<: Tuple{X, Y, Z, Vol}}}) where {X, Y, Z, Vol} = VolumeLike() -conversion_trait(::Type{<: Contour{<: Tuple{<: AbstractArray{T, 3}}}}) where T = VolumeLike() +conversion_trait(::Type{<: Contour3d}) = VertexGrid() +conversion_trait(::Type{<: Contour}) = VertexGrid() +conversion_trait(::Type{<:Contour}, x, y, z, ::Union{Function, AbstractArray{<: Number, 3}}) = VolumeLike() +conversion_trait(::Type{<: Contour}, ::AbstractArray{<: Number, 3}) = VolumeLike() function plot!(plot::Contour{<: Tuple{X, Y, Z, Vol}}) where {X, Y, Z, Vol} x, y, z, volume = plot[1:4] @@ -145,7 +145,7 @@ function plot!(plot::Contour{<: Tuple{X, Y, Z, Vol}}) where {X, Y, Z, Vol} end end - attr = Attributes(plot) + attr = copy(Attributes(plot)) attr[:colorrange] = cliprange attr[:colormap] = cmap attr[:algorithm] = 7 @@ -247,10 +247,12 @@ function plot!(plot::T) where T <: Union{Contour, Contour3d} align = (:center, :center), fontsize = labelsize, font = labelfont, + transform_marker = false ) - lift(scene.camera.projectionview, scene.px_area, labels, labelcolor, labelformatter, - lev_pos_col) do _, _, labels, labelcolor, labelformatter, lev_pos_col + lift(scene.camera.projectionview, transformationmatrix(plot), scene.viewport, + labels, labelcolor, labelformatter, lev_pos_col + ) do _, _, _, labels, labelcolor, labelformatter, lev_pos_col labels || return pos = texts.positions.val; empty!(pos) rot = texts.rotation.val; empty!(rot) diff --git a/src/basic_recipes/convenience_functions.jl b/src/basic_recipes/convenience_functions.jl index 66d8b9702a7..dab4e6d0947 100644 --- a/src/basic_recipes/convenience_functions.jl +++ b/src/basic_recipes/convenience_functions.jl @@ -16,7 +16,7 @@ end showgradients( cgrads::AbstractVector{Symbol}; h = 0.0, offset = 0.2, fontsize = 0.7, - resolution = (800, length(cgrads) * 84) + size = (800, length(cgrads) * 84) )::Scene Plots the given colour gradients arranged as horizontal colourbars. @@ -27,11 +27,11 @@ function showgradients( h = 0.0, offset = 0.4, fontsize = 0.7, - resolution = (800, length(cgrads) * 84), + size = (800, length(cgrads) * 84), monospace = true )::Scene - scene = Scene(resolution = resolution) + scene = Scene(size = resolution) map(collect(cgrads)) do cmap c = to_colormap(cmap) diff --git a/src/basic_recipes/datashader.jl b/src/basic_recipes/datashader.jl index 27123b672be..8b7a8a79270 100644 --- a/src/basic_recipes/datashader.jl +++ b/src/basic_recipes/datashader.jl @@ -377,8 +377,8 @@ end function Makie.plot!(p::DataShader{<: Tuple{<: AbstractVector{<: Point}}}) scene = parent_scene(p) limits = lift(projview_to_2d_limits, p, scene.camera.projectionview; ignore_equal_values=true) - px_area = lift(identity, p, scene.px_area; ignore_equal_values=true) - canvas = canvas_obs(limits, px_area, p.agg, p.binsize) + viewport = lift(identity, p, scene.viewport; ignore_equal_values=true) + canvas = canvas_obs(limits, viewport, p.agg, p.binsize) p._boundingbox = lift(fast_bb, p.points, p.point_transform) on_func = p.async[] ? onany_latest : onany canvas_with_aggregation = Observable(canvas[]) # Canvas that only gets notified after get_aggregation happened @@ -432,8 +432,8 @@ end function Makie.plot!(p::DataShader{<:Tuple{Dict{String, Vector{Point{2, Float32}}}}}) scene = parent_scene(p) limits = lift(projview_to_2d_limits, p, scene.camera.projectionview; ignore_equal_values=true) - px_area = lift(identity, p, scene.px_area; ignore_equal_values=true) - canvas = canvas_obs(limits, px_area, Observable(AggCount{Float32}()), p.binsize) + viewport = lift(identity, p, scene.viewport; ignore_equal_values=true) + canvas = canvas_obs(limits, viewport, Observable(AggCount{Float32}()), p.binsize) p._boundingbox = lift(p.points, p.point_transform) do cats, func rects = map(points -> fast_bb(points, func), values(cats)) return reduce(union, rects) @@ -468,7 +468,7 @@ end data_limits(p::DataShader) = p._boundingbox[] -used_attributes(::Type{<:Any}, ::Canvas) = (:operation, :local_operation) +used_attributes(::Canvas) = (:operation, :local_operation) function convert_arguments(P::Type{<:Union{MeshScatter,Image,Surface,Contour,Contour3d}}, canvas::Canvas; operation=automatic, local_operation=identity) diff --git a/src/basic_recipes/error_and_rangebars.jl b/src/basic_recipes/error_and_rangebars.jl index e8c11cb98e6..95dc2ec39e1 100644 --- a/src/basic_recipes/error_and_rangebars.jl +++ b/src/basic_recipes/error_and_rangebars.jl @@ -28,7 +28,8 @@ $(ATTRIBUTES) colorscale = identity, colorrange = automatic, inspectable = theme(scene, :inspectable), - transparency = false + transparency = false, + cycle = [:color] ) end @@ -57,7 +58,8 @@ $(ATTRIBUTES) colorscale = identity, colorrange = automatic, inspectable = theme(scene, :inspectable), - transparency = false + transparency = false, + cycle = [:color] ) end @@ -198,7 +200,7 @@ function _plot_bars!(plot, linesegpairs, is_in_y_direction) scene = parent_scene(plot) whiskers = lift(plot, linesegpairs, scene.camera.projectionview, plot.model, - scene.px_area, transform_func(plot), whiskerwidth) do endpoints, _, _, _, _, whiskerwidth + scene.viewport, transform_func(plot), whiskerwidth) do endpoints, _, _, _, _, whiskerwidth screenendpoints = plot_to_screen(plot, endpoints) diff --git a/src/basic_recipes/scatterlines.jl b/src/basic_recipes/scatterlines.jl index b6b0327e17a..3808e6ae01a 100644 --- a/src/basic_recipes/scatterlines.jl +++ b/src/basic_recipes/scatterlines.jl @@ -29,11 +29,11 @@ $(ATTRIBUTES) end -function plot!(p::Combined{scatterlines, <:NTuple{N, Any}}) where N +function plot!(p::Plot{scatterlines, <:NTuple{N, Any}}) where N # markercolor is the same as linecolor if left automatic # RGBColors -> union of all colortypes that `to_color` accepts + returns - real_markercolor = Observable{RGBColors}() + real_markercolor = Observable{RGBColors}() map!(real_markercolor, p.color, p.markercolor) do col, mcol if mcol === automatic return to_color(col) diff --git a/src/basic_recipes/streamplot.jl b/src/basic_recipes/streamplot.jl index 9a999d27300..6fd20d88d3b 100644 --- a/src/basic_recipes/streamplot.jl +++ b/src/basic_recipes/streamplot.jl @@ -194,7 +194,7 @@ function plot!(p::StreamPlot) # Calculate arrow head rotations as angles. To avoid distortions from # (extreme) aspect ratios we need to project to pixel space and renormalize. scene = parent_scene(p) - rotations = lift(p, scene.camera.projectionview, scene.px_area, data) do pv, pxa, data + rotations = lift(p, scene.camera.projectionview, scene.viewport, data) do pv, pxa, data angles = map(data[1], data[2]) do pos, dir pstart = project(scene, pos) pstop = project(scene, pos + dir) diff --git a/src/basic_recipes/text.jl b/src/basic_recipes/text.jl index d713e45c7b8..ef23d1d14d5 100644 --- a/src/basic_recipes/text.jl +++ b/src/basic_recipes/text.jl @@ -1,13 +1,20 @@ +function check_textsize_deprecation(@nospecialize(dictlike)) + if haskey(dictlike, :textsize) + throw(ArgumentError("The attribute `textsize` has been renamed to `fontsize` in Makie v0.19. Please change all occurrences of `textsize` to `fontsize` or revert back to an earlier version.")) + end +end + function plot!(plot::Text) + check_textsize_deprecation(plot) positions = plot[1] # attach a function to any text that calculates the glyph layout and stores it - glyphcollections = Observable(GlyphCollection[]) - linesegs = Observable(Point2f[]) - linewidths = Observable(Float32[]) - linecolors = Observable(RGBAf[]) + glyphcollections = Observable(GlyphCollection[]; ignore_equal_values=true) + linesegs = Observable(Point2f[]; ignore_equal_values=true) + linewidths = Observable(Float32[]; ignore_equal_values=true) + linecolors = Observable(RGBAf[]; ignore_equal_values=true) lineindices = Ref(Int[]) - onany(plot.text, plot.fontsize, plot.font, plot.fonts, plot.align, + onany(plot, plot.text, plot.fontsize, plot.font, plot.fonts, plot.align, plot.rotation, plot.justification, plot.lineheight, plot.calculated_colors, plot.strokecolor, plot.strokewidth, plot.word_wrap_width, plot.offset) do str, ts, f, fs, al, rot, jus, lh, col, scol, swi, www, offs @@ -23,7 +30,8 @@ function plot!(plot::Text) lwidths = Float32[] lcolors = RGBAf[] lindices = Int[] - function push_args((gc, ls, lw, lc, lindex)) + function push_args(args...) + gc, ls, lw, lc, lindex = _get_glyphcollection_and_linesegments(args...) push!(gcs, gc) append!(lsegs, ls) append!(lwidths, lw) @@ -31,18 +39,15 @@ function plot!(plot::Text) append!(lindices, lindex) return end - func = push_args ∘ _get_glyphcollection_and_linesegments if str isa Vector # If we have a Vector of strings, Vector arguments are interpreted # as per string. - broadcast_foreach( - func, - str, 1:attr_broadcast_length(str), ts, f, fs, al, rot, jus, lh, col, scol, swi, www, offs + broadcast_foreach(push_args, str, 1:attr_broadcast_length(str), ts, f, fs, al, rot, jus, lh, col, scol, swi, www, offs ) else # Otherwise Vector arguments are interpreted by layout_text/ # glyph_collection as per character. - func(str, 1, ts, f, fs, al, rot, jus, lh, col, scol, swi, www, offs) + push_args(str, 1, ts, f, fs, al, rot, jus, lh, col, scol, swi, www, offs) end glyphcollections[] = gcs linewidths[] = lwidths @@ -51,11 +56,11 @@ function plot!(plot::Text) linesegs[] = lsegs end - linesegs_shifted = Observable(Point2f[]) + linesegs_shifted = Observable(Point2f[]; ignore_equal_values=true) sc = parent_scene(plot) - onany(linesegs, positions, sc.camera.projectionview, sc.px_area, + onany(plot, linesegs, positions, sc.camera.projectionview, sc.viewport, transform_func_obs(sc), get(plot, :space, :data)) do segs, pos, _, _, transf, space pos_transf = plot_to_screen(plot, pos) linesegs_shifted[] = map(segs, lineindices[]) do seg, index @@ -157,7 +162,7 @@ function plot!(plot::Text{<:Tuple{<:AbstractArray{<:Tuple{<:Any, <:Point}}}}) text!(plot, positions; text = strings, attrs...) # update both text and positions together - on(strings_and_positions) do str_pos + on(plot, strings_and_positions) do str_pos strs = first.(str_pos) poss = to_ndim.(Ref(Point3f), last.(str_pos), 0) diff --git a/src/basic_recipes/tooltip.jl b/src/basic_recipes/tooltip.jl index ae0a516f04c..74c75241062 100644 --- a/src/basic_recipes/tooltip.jl +++ b/src/basic_recipes/tooltip.jl @@ -36,13 +36,13 @@ Creates a tooltip pointing at `position` displaying the given `string` """ @recipe(Tooltip, position) do scene Attributes(; - # General - text = "", + # General + text = "", offset = 10, placement = :above, align = 0.5, - xautolimits = false, - yautolimits = false, + xautolimits = false, + yautolimits = false, zautolimits = false, overdraw = false, depth_shift = 0f0, @@ -62,7 +62,7 @@ Creates a tooltip pointing at `position` displaying the given `string` # Background backgroundcolor = :white, triangle_size = 10, - + # Outline outline_color = :black, outline_linewidth = 2f0, @@ -103,7 +103,7 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) text_offset = map(p.offset, textpadding, p.triangle_size, p.placement, p.align) do o, pad, ts, placement, align l, r, b, t = pad - if placement === :left + if placement === :left return Vec2f(-o - r - ts, b - align * (b + t)) elseif placement === :right return Vec2f( o + l + ts, b - align * (b + t)) @@ -118,7 +118,7 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) end text_align = map(p.placement, p.align) do placement, align - if placement === :left + if placement === :left return (1.0, align) elseif placement === :right return (0.0, align) @@ -155,9 +155,9 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) # Text background mesh mesh!( - p, bbox, shading = false, space = :pixel, + p, bbox, shading = NoShading, space = :pixel, color = p.backgroundcolor, fxaa = false, - transparency = p.transparency, visible = p.visible, + transparency = p.transparency, visible = p.visible, overdraw = p.overdraw, depth_shift = p.depth_shift, inspectable = p.inspectable ) @@ -170,8 +170,8 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) ) mp = mesh!( - p, triangle, shading = false, space = :pixel, - color = p.backgroundcolor, + p, triangle, shading = NoShading, space = :pixel, + color = p.backgroundcolor, transparency = p.transparency, visible = p.visible, overdraw = p.overdraw, depth_shift = p.depth_shift, inspectable = p.inspectable @@ -179,8 +179,8 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) onany(bbox, p.triangle_size, p.placement, p.align) do bb, s, placement, align o = origin(bb); w = widths(bb) scale!(mp, s, s, s) - - if placement === :left + + if placement === :left translate!(mp, Vec3f(o[1] + w[1], o[2] + align * w[2], 0)) rotate!(mp, qrotation(Vec3f(0,0,1), 0.5pi)) elseif placement === :right @@ -212,45 +212,45 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) # | ____ # | | - shift = if placement === :left + shift = if placement === :left Vec2f[ - (l, b + 0.5h), (l, t), (r, t), - (r, b + align * h + 0.5s), - (r + s, b + align * h), + (l, b + 0.5h), (l, t), (r, t), + (r, b + align * h + 0.5s), + (r + s, b + align * h), (r, b + align * h - 0.5s), (r, b), (l, b), (l, b + 0.5h) ] elseif placement === :right Vec2f[ - (l + 0.5w, b), (l, b), - (l, b + align * h - 0.5s), - (l-s, b + align * h), + (l + 0.5w, b), (l, b), + (l, b + align * h - 0.5s), + (l-s, b + align * h), (l, b + align * h + 0.5s), (l, t), (r, t), (r, b), (l + 0.5w, b) ] elseif placement in (:below, :down, :bottom) Vec2f[ - (l, b + 0.5h), (l, t), - (l + align * w - 0.5s, t), - (l + align * w, t+s), - (l + align * w + 0.5s, t), + (l, b + 0.5h), (l, t), + (l + align * w - 0.5s, t), + (l + align * w, t+s), + (l + align * w + 0.5s, t), (r, t), (r, b), (l, b), (l, b + 0.5h) ] elseif placement in (:above, :up, :top) Vec2f[ - (l, b + 0.5h), (l, t), (r, t), (r, b), - (l + align * w + 0.5s, b), - (l + align * w, b-s), - (l + align * w - 0.5s, b), + (l, b + 0.5h), (l, t), (r, t), (r, b), + (l + align * w + 0.5s, b), + (l + align * w, b-s), + (l + align * w - 0.5s, b), (l, b), (l, b + 0.5h) ] else @error "Tooltip placement $placement invalid. Assuming :above" Vec2f[ - (l, b + 0.5h), (l, t), (r, t), (r, b), - (l + align * w + 0.5s, b), - (l + align * w, b-s), - (l + align * w - 0.5s, b), + (l, b + 0.5h), (l, t), (r, t), (r, b), + (l + align * w + 0.5s, b), + (l + align * w, b-s), + (l + align * w - 0.5s, b), (l, b), (l, b + 0.5h) ] end @@ -259,8 +259,8 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) end lines!( - p, outline, - color = p.outline_color, space = :pixel, + p, outline, + color = p.outline_color, space = :pixel, linewidth = p.outline_linewidth, linestyle = p.outline_linestyle, transparency = p.transparency, visible = p.visible, overdraw = p.overdraw, depth_shift = p.depth_shift, diff --git a/src/basic_recipes/tricontourf.jl b/src/basic_recipes/tricontourf.jl index e808c34103e..e711345735d 100644 --- a/src/basic_recipes/tricontourf.jl +++ b/src/basic_recipes/tricontourf.jl @@ -4,7 +4,7 @@ struct DelaunayTriangulation end tricontourf(triangles::Triangulation, zs; kwargs...) tricontourf(xs, ys, zs; kwargs...) -Plots a filled tricontour of the height information in `zs` at the horizontal positions `xs` and +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. @@ -54,16 +54,16 @@ function Makie.used_attributes(::Type{<:Tricontourf}, ::AbstractVector{<:Real}, return (:triangulation,) end -function Makie.convert_arguments(::Type{<:Tricontourf}, x::AbstractVector{<:Real}, y::AbstractVector{<:Real}, z::AbstractVector{<:Real}; +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' + # 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) @@ -198,7 +198,6 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:DelTri.Triangulation, <:AbstractVe color = colors, strokewidth = 0, strokecolor = :transparent, - shading = false, inspectable = c.inspectable, transparency = c.transparency ) diff --git a/src/basic_recipes/triplot.jl b/src/basic_recipes/triplot.jl index d4e72e34284..d2ccc94ffc6 100644 --- a/src/basic_recipes/triplot.jl +++ b/src/basic_recipes/triplot.jl @@ -189,8 +189,9 @@ function Makie.plot!(p::Triplot{<:Tuple{<:Vector{<:Point}}}) return DelTri.triangulate(transformed) end - transform = Transformation(p.transformation; transform_func=identity) - return triplot!(p, attr, tri; transformation=transform) + attr[:transformation] = Transformation(p.transformation; transform_func=identity) + triplot!(p, attr, tri) + return end function Makie.plot!(p::Triplot{<:Tuple{<:DelTri.Triangulation}}) @@ -237,4 +238,4 @@ function Makie.plot!(p::Triplot{<:Tuple{<:DelTri.Triangulation}}) scatter!(p, present_points_2f; markersize=p.markersize, color=p.markercolor, strokecolor=p.strokecolor, marker=p.marker, visible=p.show_points, depth_shift=-3.0f-5) return p -end \ No newline at end of file +end diff --git a/src/basic_recipes/voronoiplot.jl b/src/basic_recipes/voronoiplot.jl index 0a2dd133ec3..7ce3d6f87b2 100644 --- a/src/basic_recipes/voronoiplot.jl +++ b/src/basic_recipes/voronoiplot.jl @@ -163,9 +163,8 @@ function plot!(p::Voronoiplot{<:Tuple{<:Vector{<:Point{N}}}}) where {N} return bb end end - - transform = Transformation(p.transformation; transform_func=identity) - return voronoiplot!(p, attr, vorn; transformation=transform) + attr[:transformation] = Transformation(p.transformation; transform_func=identity) + return voronoiplot!(p, attr, vorn) end function data_limits(p::Voronoiplot{<:Tuple{<:Vector{<:Point{N}}}}) where {N} diff --git a/src/bezier.jl b/src/bezier.jl index 80a2d5d9367..a7754525fea 100644 --- a/src/bezier.jl +++ b/src/bezier.jl @@ -1,29 +1,31 @@ using StableHashTraits +const Point2d = Point2{Float64} + struct MoveTo - p::Point2{Float64} + p::Point2d end -MoveTo(x, y) = MoveTo(Point(x, y)) +MoveTo(x, y) = MoveTo(Point2d(x, y)) struct LineTo - p::Point2{Float64} + p::Point2d end -LineTo(x, y) = LineTo(Point(x, y)) +LineTo(x, y) = LineTo(Point2d(x, y)) struct CurveTo - c1::Point2{Float64} - c2::Point2{Float64} - p::Point2{Float64} + c1::Point2d + c2::Point2d + p::Point2d end CurveTo(cx1, cy1, cx2, cy2, p1, p2) = CurveTo( - Point(cx1, cy1), Point(cx2, cy2), Point(p1, p2) + Point2d(cx1, cy1), Point2d(cx2, cy2), Point2d(p1, p2) ) struct EllipticalArc - c::Point2{Float64} + c::Point2d r1::Float64 r2::Float64 angle::Float64 @@ -31,16 +33,88 @@ struct EllipticalArc a2::Float64 end -EllipticalArc(cx, cy, r1, r2, angle, a1, a2) = EllipticalArc(Point(cx, cy), +EllipticalArc(cx, cy, r1, r2, angle, a1, a2) = EllipticalArc(Point2d(cx, cy), r1, r2, angle, a1, a2) struct ClosePath end - const PathCommand = Union{MoveTo, LineTo, CurveTo, EllipticalArc, ClosePath} +function bbox(commands::Vector{PathCommand}) + prev = commands[1] + bb = nothing + for comm in @view(commands[2:end]) + if comm isa MoveTo || comm isa ClosePath + continue + else + endp = endpoint(prev) + _bb = cleanup_bbox(bbox(endp, comm)) + bb = bb === nothing ? _bb : union(bb, _bb) + end + prev = comm + end + return bb +end + +function elliptical_arc_to_beziers(arc::EllipticalArc) + delta_a = abs(arc.a2 - arc.a1) + n_beziers = ceil(Int, delta_a / 0.5pi) + angles = range(arc.a1, arc.a2; length=n_beziers + 1) + + startpoint = Point2f(cos(arc.a1), sin(arc.a1)) + curves = map(angles[1:(end - 1)], angles[2:end]) do start, stop + theta = stop - start + kappa = 4 / 3 * tan(theta / 4) + c1 = Point2f(cos(start) - kappa * sin(start), sin(start) + kappa * cos(start)) + c2 = Point2f(cos(stop) + kappa * sin(stop), sin(stop) - kappa * cos(stop)) + b = Point2f(cos(stop), sin(stop)) + return CurveTo(c1, c2, b) + end + + path = BezierPath([LineTo(startpoint), curves...]) + path = scale(path, Vec2{Float64}(arc.r1, arc.r2)) + path = rotate(path, arc.angle) + return translate(path, arc.c) +end + +bbox(p, x::Union{LineTo,CurveTo}) = bbox(segment(p, x)) +function bbox(p, e::EllipticalArc) + return bbox(elliptical_arc_to_beziers(e)) +end + +endpoint(m::MoveTo) = m.p +endpoint(l::LineTo) = l.p +endpoint(c::CurveTo) = c.p +function endpoint(e::EllipticalArc) + return point_at_angle(e, e.a2) +end + +function point_at_angle(e::EllipticalArc, theta) + M = abs(e.r1) * cos(theta) + N = abs(e.r2) * sin(theta) + return Point2f(e.c[1] + cos(e.angle) * M - sin(e.angle) * N, + e.c[2] + sin(e.angle) * M + cos(e.angle) * N) +end + +function cleanup_bbox(bb::Rect2f) + if any(x -> x < 0, bb.widths) + p = bb.origin .+ (bb.widths .< 0) .* bb.widths + return Rect2f(p, abs.(bb.widths)) + end + return bb +end + struct BezierPath commands::Vector{PathCommand} + boundingbox::Rect2f + hash::UInt32 + function BezierPath(commands::Vector) + c = convert(Vector{PathCommand}, commands) + return new(c, bbox(c), StableHashTraits.stable_hash(c; alg=crc32c, version=2)) + end end +bbox(x::BezierPath) = x.boundingbox +fast_stable_hash(x::BezierPath) = x.hash + # so that the same bezierpath with a different instance of a vector hashes the same # and we don't create the same texture atlas entry twice @@ -52,7 +126,7 @@ function Base.:+(pc::P, p::Point2) where P <: PathCommand return P(map(f -> getfield(pc, f) + p, fnames)...) end -scale(bp::BezierPath, s::Real) = BezierPath([scale(x, Vec(s, s)) for x in bp.commands]) +scale(bp::BezierPath, s::Real) = BezierPath([scale(x, Vec2{Float64}(s, s)) for x in bp.commands]) scale(bp::BezierPath, v::VecTypes{2}) = BezierPath([scale(x, v) for x in bp.commands]) translate(bp::BezierPath, v::VecTypes{2}) = BezierPath([translate(x, v) for x in bp.commands]) @@ -114,7 +188,7 @@ function fit_to_bbox(b::BezierPath, bb_target::Rect2; keep_aspect = true) scale_factor end - bb_t = translate(scale(translate(b, -center_path), scale_factor_aspect), center_target) + return translate(scale(translate(b, -center_path), scale_factor_aspect), center_target) end function fit_to_unit_square(b::BezierPath, keep_aspect = true) @@ -127,74 +201,13 @@ Base.:+(bp::BezierPath, p::Point2) = BezierPath(bp.commands .+ Ref(p)) # markers that fit into a square with sidelength 1 centered on (0, 0) -const BezierCircle = let - r = 0.47 # sqrt(1/pi) - BezierPath([ - MoveTo(Point(r, 0.0)), - EllipticalArc(Point(0.0, 0), r, r, 0.0, 0.0, 2pi), - ClosePath(), - ]) -end - -const BezierUTriangle = let - aspect = 1 - h = 0.97 # sqrt(aspect) * sqrt(2) - w = 0.97 # 1/sqrt(aspect) * sqrt(2) - # r = Float32(sqrt(1 / (3 * sqrt(3) / 4))) - p1 = Point(0, h/2) - p2 = Point2(-w/2, -h/2) - p3 = Point2(w/2, -h/2) - centroid = (p1 + p2 + p3) / 3 - bp = BezierPath([ - MoveTo(p1 - centroid), - LineTo(p2 - centroid), - LineTo(p3 - centroid), - ClosePath() - ]) -end - -const BezierLTriangle = rotate(BezierUTriangle, pi/2) -const BezierDTriangle = rotate(BezierUTriangle, pi) -const BezierRTriangle = rotate(BezierUTriangle, 3pi/2) - - -const BezierSquare = let - r = 0.95 * sqrt(pi)/2/2 # this gives a little less area as the r=0.5 circle - BezierPath([ - MoveTo(Point2(r, -r)), - LineTo(Point2(r, r)), - LineTo(Point2(-r, r)), - LineTo(Point2(-r, -r)), - ClosePath() - ]) -end - -const BezierCross = let - cutfraction = 2/3 - r = 0.5 # 1/(2 * sqrt(1 - cutfraction^2)) - ri = 0.166 #r * (1 - cutfraction) - - first_three = Point2[(r, ri), (ri, ri), (ri, r)] - all = map(0:pi/2:3pi/2) do a - m = Mat2f(sin(a), cos(a), cos(a), -sin(a)) - Ref(m) .* first_three - end |> x -> reduce(vcat, x) - - BezierPath([ - MoveTo(all[1]), - LineTo.(all[2:end])..., - ClosePath() - ]) -end - -const BezierX = rotate(BezierCross, pi/4) function bezier_ngon(n, radius, angle) points = [radius * Point2f(cos(a + angle), sin(a + angle)) for a in range(0, 2pi, length = n+1)[1:end-1]] BezierPath([ MoveTo(points[1]); - LineTo.(points[2:end]); + LineTo.(@view points[2:end]); ClosePath() ]) end @@ -239,10 +252,10 @@ function BezierPath(svg::AbstractString; fit = false, bbox = nothing, flipy = fa commands = parse_bezier_commands(svg) p = BezierPath(commands) if flipy - p = scale(p, Vec(1, -1)) + p = scale(p, Vec2{Float64}(1, -1)) end if flipx - p = scale(p, Vec(-1, 1)) + p = scale(p, Vec2{Float64}(-1, 1)) end if fit if bbox === nothing @@ -265,7 +278,7 @@ function parse_bezier_commands(svg) lastcomm = nothing function lastp() if isnothing(lastcomm) - Point(0, 0) + Point2d(0, 0) else c = commands[end] if c isa ClosePath @@ -282,10 +295,10 @@ function parse_bezier_commands(svg) rx = c.r1 ry = c.r2 m = Mat2(cos(ϕ), sin(ϕ), -sin(ϕ), cos(ϕ)) - m * Point(rx * cos(a2), ry * sin(a2)) + c.c + return m * Point2d(rx * cos(a2), ry * sin(a2)) + c.c end else - c.p + return c.p end end end @@ -302,27 +315,27 @@ function parse_bezier_commands(svg) if comm == "M" x, y = parse.(Float64, args[i+1:i+2]) - push!(commands, MoveTo(Point2(x, y))) + push!(commands, MoveTo(Point2d(x, y))) i += 3 elseif comm == "m" x, y = parse.(Float64, args[i+1:i+2]) - push!(commands, MoveTo(Point2(x, y) + lastp())) + push!(commands, MoveTo(Point2d(x, y) + lastp())) i += 3 elseif comm == "L" x, y = parse.(Float64, args[i+1:i+2]) - push!(commands, LineTo(Point2(x, y))) + push!(commands, LineTo(Point2d(x, y))) i += 3 elseif comm == "l" x, y = parse.(Float64, args[i+1:i+2]) - push!(commands, LineTo(Point2(x, y) + lastp())) + push!(commands, LineTo(Point2d(x, y) + lastp())) i += 3 elseif comm == "H" x = parse(Float64, args[i+1]) - push!(commands, LineTo(Point2(x, lastp()[2]))) + push!(commands, LineTo(Point2d(x, lastp()[2]))) i += 2 elseif comm == "h" x = parse(Float64, args[i+1]) - push!(commands, LineTo(X(x) + lastp())) + push!(commands, LineTo(Point2d(x, 0) + lastp())) i += 2 elseif comm == "Z" push!(commands, ClosePath()) @@ -332,25 +345,25 @@ function parse_bezier_commands(svg) i += 1 elseif comm == "C" x1, y1, x2, y2, x3, y3 = parse.(Float64, args[i+1:i+6]) - push!(commands, CurveTo(Point2(x1, y1), Point2(x2, y2), Point2(x3, y3))) + push!(commands, CurveTo(Point2d(x1, y1), Point2d(x2, y2), Point2d(x3, y3))) i += 7 elseif comm == "c" x1, y1, x2, y2, x3, y3 = parse.(Float64, args[i+1:i+6]) l = lastp() - push!(commands, CurveTo(Point2(x1, y1) + l, Point2(x2, y2) + l, Point2(x3, y3) + l)) + push!(commands, CurveTo(Point2d(x1, y1) + l, Point2d(x2, y2) + l, Point2d(x3, y3) + l)) i += 7 elseif comm == "S" x1, y1, x2, y2 = parse.(Float64, args[i+1:i+4]) prev = commands[end] reflected = prev.p + (prev.p - prev.c2) - push!(commands, CurveTo(reflected, Point2(x1, y1), Point2(x2, y2))) + push!(commands, CurveTo(reflected, Point2d(x1, y1), Point2d(x2, y2))) i += 5 elseif comm == "s" x1, y1, x2, y2 = parse.(Float64, args[i+1:i+4]) prev = commands[end] reflected = prev.p + (prev.p - prev.c2) l = lastp() - push!(commands, CurveTo(reflected, Point2(x1, y1) + l, Point2(x2, y2) + l)) + push!(commands, CurveTo(reflected, Point2d(x1, y1) + l, Point2d(x2, y2) + l)) i += 5 elseif comm == "A" args[i+1:i+7] @@ -376,12 +389,12 @@ function parse_bezier_commands(svg) elseif comm == "v" dy = parse(Float64, args[i+1]) l = lastp() - push!(commands, LineTo(Point2(l[1], l[2] + dy))) + push!(commands, LineTo(Point2d(l[1], l[2] + dy))) i += 2 elseif comm == "V" y = parse(Float64, args[i+1]) l = lastp() - push!(commands, LineTo(Point2(l[1], y))) + push!(commands, LineTo(Point2d(l[1], y))) i += 2 else for c in commands @@ -400,8 +413,8 @@ end function EllipticalArc(x1, y1, x2, y2, rx, ry, ϕ, largearc::Bool, sweepflag::Bool) # https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes - p1 = Point(x1, y1) - p2 = Point(x2, y2) + p1 = Point2d(x1, y1) + p2 = Point2d(x2, y2) m1 = Mat2(cos(ϕ), -sin(ϕ), sin(ϕ), cos(ϕ)) x1′, y1′ = m1 * (0.5 * (p1 - p2)) @@ -410,16 +423,16 @@ function EllipticalArc(x1, y1, x2, y2, rx, ry, ϕ, largearc::Bool, sweepflag::Bo (rx^2 * y1′^2 + ry^2 * x1′^2) c′ = (largearc == sweepflag ? -1 : 1) * - sqrt(tempsqrt) * Point(rx * y1′ / ry, -ry * x1′ / rx) + sqrt(tempsqrt) * Point2d(rx * y1′ / ry, -ry * x1′ / rx) c = Mat2(cos(ϕ), sin(ϕ), -sin(ϕ), cos(ϕ)) * c′ + 0.5 * (p1 + p2) vecangle(u, v) = sign(u[1] * v[2] - u[2] * v[1]) * acos(dot(u, v) / (norm(u) * norm(v))) - px(sign) = Point((sign * x1′ - c′[1]) / rx, (sign * y1′ - c′[2]) / rx) + px(sign) = Point2d((sign * x1′ - c′[1]) / rx, (sign * y1′ - c′[2]) / rx) - θ1 = vecangle(Point(1.0, 0.0), px(1)) + θ1 = vecangle(Point2d(1.0, 0.0), px(1)) Δθ_pre = mod(vecangle(px(1), px(-1)), 2pi) Δθ = if Δθ_pre > 0 && !sweepflag Δθ_pre - 2pi @@ -442,7 +455,6 @@ function make_outline(path) points = FT_Vector[] tags = Int8[] contours = Int16[] - flags = Int32(0) for command in path.commands new_contour, n_newpoints, newpoints, newtags = convert_command(command) if new_contour @@ -581,60 +593,12 @@ struct LineSegment to::Point2f end -function bbox(b::BezierPath) - prev = b.commands[1] - bb = nothing - for comm in b.commands[2:end] - if comm isa MoveTo || comm isa ClosePath - continue - else - endp = endpoint(prev) - _bb = cleanup_bbox(bbox(endp, comm)) - bb = bb === nothing ? _bb : union(bb, _bb) - end - prev = comm - end - bb -end - -segment(p, l::LineTo) = LineSegment(p, l.p) -segment(p, c::CurveTo) = BezierSegment(p, c.c1, c.c2, c.p) - -endpoint(m::MoveTo) = m.p -endpoint(l::LineTo) = l.p -endpoint(c::CurveTo) = c.p -function endpoint(e::EllipticalArc) - point_at_angle(e, e.a2) -end - -function point_at_angle(e::EllipticalArc, theta) - M = abs(e.r1) * cos(theta) - N = abs(e.r2) * sin(theta) - Point2f( - e.c[1] + cos(e.angle) * M - sin(e.angle) * N, - e.c[2] + sin(e.angle) * M + cos(e.angle) * N - ) -end - -function cleanup_bbox(bb::Rect2f) - if any(x -> x < 0, bb.widths) - p = bb.origin .+ (bb.widths .< 0) .* bb.widths - return Rect2f(p, abs.(bb.widths)) - end - return bb -end - -bbox(p, x::Union{LineTo, CurveTo}) = bbox(segment(p, x)) -function bbox(p, e::EllipticalArc) - bbox(elliptical_arc_to_beziers(e)) -end function bbox(ls::LineSegment) - Rect2f(ls.from, ls.to - ls.from) + return Rect2f(ls.from, ls.to - ls.from) end function bbox(b::BezierSegment) - p0 = b.from p1 = b.c1 p2 = b.c2 @@ -644,68 +608,103 @@ function bbox(b::BezierSegment) ma = [max.(p0, p3)...] c = -p0 + p1 - b = p0 - 2p1 + p2 + b = p0 - 2p1 + p2 a = -p0 + 3p1 - 3p2 + 1p3 - h = [(b.*b - a.*c)...] + h = [(b .* b - a .* c)...] if h[1] > 0 h[1] = sqrt(h[1]) t = (-b[1] - h[1]) / a[1] if t > 0 && t < 1 - s = 1.0-t - q = s*s*s*p0[1] + 3.0*s*s*t*p1[1] + 3.0*s*t*t*p2[1] + t*t*t*p3[1] - mi[1] = min(mi[1],q) - ma[1] = max(ma[1],q) + s = 1.0 - t + q = s * s * s * p0[1] + 3.0 * s * s * t * p1[1] + 3.0 * s * t * t * p2[1] + t * t * t * p3[1] + mi[1] = min(mi[1], q) + ma[1] = max(ma[1], q) end - t = (-b[1] + h[1])/a[1] - if t>0 && t<1 - s = 1.0-t - q = s*s*s*p0[1] + 3.0*s*s*t*p1[1] + 3.0*s*t*t*p2[1] + t*t*t*p3[1] - mi[1] = min(mi[1],q) - ma[1] = max(ma[1],q) + t = (-b[1] + h[1]) / a[1] + if t > 0 && t < 1 + s = 1.0 - t + q = s * s * s * p0[1] + 3.0 * s * s * t * p1[1] + 3.0 * s * t * t * p2[1] + t * t * t * p3[1] + mi[1] = min(mi[1], q) + ma[1] = max(ma[1], q) end end - if h[2]>0.0 + if h[2] > 0.0 h[2] = sqrt(h[2]) - t = (-b[2] - h[2])/a[2] - if t>0.0 && t<1.0 - s = 1.0-t - q = s*s*s*p0[2] + 3.0*s*s*t*p1[2] + 3.0*s*t*t*p2[2] + t*t*t*p3[2] - mi[2] = min(mi[2],q) - ma[2] = max(ma[2],q) + t = (-b[2] - h[2]) / a[2] + if t > 0.0 && t < 1.0 + s = 1.0 - t + q = s * s * s * p0[2] + 3.0 * s * s * t * p1[2] + 3.0 * s * t * t * p2[2] + t * t * t * p3[2] + mi[2] = min(mi[2], q) + ma[2] = max(ma[2], q) end - t = (-b[2] + h[2])/a[2] - if t>0.0 && t<1.0 - s = 1.0-t - q = s*s*s*p0[2] + 3.0*s*s*t*p1[2] + 3.0*s*t*t*p2[2] + t*t*t*p3[2] - mi[2] = min(mi[2],q) - ma[2] = max(ma[2],q) + t = (-b[2] + h[2]) / a[2] + if t > 0.0 && t < 1.0 + s = 1.0 - t + q = s * s * s * p0[2] + 3.0 * s * s * t * p1[2] + 3.0 * s * t * t * p2[2] + t * t * t * p3[2] + mi[2] = min(mi[2], q) + ma[2] = max(ma[2], q) end end - Rect2f(Point(mi...), Point(ma...) - Point(mi...)) + return Rect2f(Point(mi...), Point(ma...) - Point(mi...)) end +segment(p, l::LineTo) = LineSegment(p, l.p) +segment(p, c::CurveTo) = BezierSegment(p, c.c1, c.c2, c.p) -function elliptical_arc_to_beziers(arc::EllipticalArc) - delta_a = abs(arc.a2 - arc.a1) - n_beziers = ceil(Int, delta_a / 0.5pi) - angles = range(arc.a1, arc.a2, length = n_beziers + 1) - startpoint = Point2f(cos(arc.a1), sin(arc.a1)) - curves = map(angles[1:end-1], angles[2:end]) do start, stop - theta = stop - start - kappa = 4/3 * tan(theta/4) - c1 = Point2f(cos(start) - kappa * sin(start), sin(start) + kappa * cos(start)) - c2 = Point2f(cos(stop) + kappa * sin(stop), sin(stop) - kappa * cos(stop)) - b = Point2f(cos(stop), sin(stop)) - CurveTo(c1, c2, b) - end +const BezierCircle = let + r = 0.47 # sqrt(1/pi) + BezierPath([MoveTo(Point(r, 0.0)), + EllipticalArc(Point(0.0, 0), r, r, 0.0, 0.0, 2pi), + ClosePath()]) +end - path = BezierPath([LineTo(startpoint), curves...]) - path = scale(path, Vec(arc.r1, arc.r2)) - path = rotate(path, arc.angle) - path = translate(path, arc.c) +const BezierUTriangle = let + aspect = 1 + h = 0.97 # sqrt(aspect) * sqrt(2) + w = 0.97 # 1/sqrt(aspect) * sqrt(2) + # r = Float32(sqrt(1 / (3 * sqrt(3) / 4))) + p1 = Point(0, h / 2) + p2 = Point2d(-w / 2, -h / 2) + p3 = Point2d(w / 2, -h / 2) + centroid = (p1 + p2 + p3) / 3 + bp = BezierPath([MoveTo(p1 - centroid), + LineTo(p2 - centroid), + LineTo(p3 - centroid), + ClosePath()]) +end + +const BezierLTriangle = rotate(BezierUTriangle, pi / 2) +const BezierDTriangle = rotate(BezierUTriangle, pi) +const BezierRTriangle = rotate(BezierUTriangle, 3pi / 2) + +const BezierSquare = let + r = 0.95 * sqrt(pi) / 2 / 2 # this gives a little less area as the r=0.5 circle + BezierPath([MoveTo(Point2d(r, -r)), + LineTo(Point2d(r, r)), + LineTo(Point2d(-r, r)), + LineTo(Point2d(-r, -r)), + ClosePath()]) +end + +const BezierCross = let + cutfraction = 2 / 3 + r = 0.5 # 1/(2 * sqrt(1 - cutfraction^2)) + ri = 0.166 #r * (1 - cutfraction) + + first_three = Point2d[(r, ri), (ri, ri), (ri, r)] + all = (x -> reduce(vcat, x))(map(0:(pi / 2):(3pi / 2)) do a + m = Mat2f(sin(a), cos(a), cos(a), -sin(a)) + return Ref(m) .* first_three + end) + + BezierPath([MoveTo(all[1]), + LineTo.(all[2:end])..., + ClosePath()]) end + +const BezierX = rotate(BezierCross, pi / 4) diff --git a/src/camera/camera.jl b/src/camera/camera.jl index 48d63063fa7..2d75f3dc9ca 100644 --- a/src/camera/camera.jl +++ b/src/camera/camera.jl @@ -1,5 +1,5 @@ function Base.copy(x::Camera) - Camera(ntuple(7) do i + Camera(ntuple(9) do i getfield(x, i) end...) end @@ -18,6 +18,7 @@ function Base.show(io::IO, camera::Camera) println(io, " projection: ", camera.projection[]) println(io, " projectionview: ", camera.projectionview[]) println(io, " resolution: ", camera.resolution[]) + println(io, " lookat: ", camera.lookat[]) println(io, " eyeposition: ", camera.eyeposition[]) end @@ -67,8 +68,8 @@ function Observables.on(f, camera::Camera, observables::AbstractObservable...; p return f end -function Camera(px_area) - pixel_space = lift(px_area) do window_size +function Camera(viewport) + pixel_space = lift(viewport) do window_size nearclip = -10_000f0 farclip = 10_000f0 w, h = Float32.(widths(window_size)) @@ -82,9 +83,11 @@ function Camera(px_area) view, proj, proj_view, - lift(a-> Vec2f(widths(a)), px_area), + lift(a-> Vec2f(widths(a)), viewport), + Observable(Vec3f(0)), Observable(Vec3f(1)), - ObserverFunction[] + ObserverFunction[], + Dict{Symbol, Observable}() ) end @@ -100,7 +103,7 @@ end is_mouseinside(x, target) = is_mouseinside(get_scene(x), target) function is_mouseinside(scene::Scene, target) scene === target && return false - Vec(scene.events.mouseposition[]) in pixelarea(scene)[] || return false + Vec(scene.events.mouseposition[]) in viewport(scene)[] || return false for child in r.children is_mouseinside(child, target) && return true end @@ -114,7 +117,7 @@ Returns true if the current mouseposition is inside the given scene. """ is_mouseinside(x) = is_mouseinside(get_scene(x)) function is_mouseinside(scene::Scene) - return Vec(scene.events.mouseposition[]) in pixelarea(scene)[] + return Vec(scene.events.mouseposition[]) in viewport(scene)[] # Check that mouse is not inside any other screen # for child in scene.children # is_mouseinside(child) && return false diff --git a/src/camera/camera2d.jl b/src/camera/camera2d.jl index ff276348d94..d9d354a180f 100644 --- a/src/camera/camera2d.jl +++ b/src/camera/camera2d.jl @@ -1,8 +1,8 @@ struct Camera2D <: AbstractCamera area::Observable{Rect2f} zoomspeed::Observable{Float32} - zoombutton::Observable{ButtonTypes} - panbutton::Observable{Union{ButtonTypes, Vector{ButtonTypes}}} + zoombutton::Observable{IsPressedInputType} + panbutton::Observable{IsPressedInputType} padding::Observable{Float32} last_area::Observable{Vec{2, Int}} update_limits::Observable{Bool} @@ -11,14 +11,23 @@ end """ cam2d!(scene::SceneLike, kwargs...) -Creates a 2D camera for the given Scene. +Creates a 2D camera for the given `scene`. The camera implements zooming by +scrolling and translation using mouse drag. It also implements rectangle +selections. + +## Keyword Arguments + +- `zoomspeed = 0.1f0` sets the zoom speed. +- `zoombutton = true` sets a button (combination) which needs to be pressed to enable zooming. By default no button needs to be pressed. +- `panbutton = Mouse.right` sets the button used to translate the camera. This must include a mouse button. +- `selectionbutton = (Keyboard.space, Mouse.left)` sets the button used for rectangle selection. This must include a mouse button. """ function cam2d!(scene::SceneLike; kw_args...) cam_attributes = merged_get!(:cam2d, scene, Attributes(kw_args)) do Attributes( area = Observable(Rectf(0, 0, 1, 1)), zoomspeed = 0.10f0, - zoombutton = nothing, + zoombutton = true, panbutton = Mouse.right, selectionbutton = (Keyboard.space, Mouse.left), padding = 0.001, @@ -37,6 +46,7 @@ function cam2d!(scene::SceneLike; kw_args...) cam end +get_space(::Camera2D) = :data wscale(screenrect, viewrect) = widths(viewrect) ./ widths(screenrect) @@ -45,7 +55,14 @@ wscale(screenrect, viewrect) = widths(viewrect) ./ widths(screenrect) Updates the camera for the given `scene` to cover the given `area` in 2d. """ -update_cam!(scene::SceneLike, area) = update_cam!(scene, cameracontrols(scene), area) +function update_cam!(scene::SceneLike, area::Rect) + return update_cam!(scene, cameracontrols(scene), area) +end +function update_cam!(scene::SceneLike, area::Rect, center::Bool) + return update_cam!(scene, cameracontrols(scene), area, center) +end + + """ update_cam!(scene::SceneLike) @@ -60,7 +77,7 @@ function update_cam!(scene::Scene, cam::Camera2D, area3d::Rect) # ignore rects with width almost 0 any(x-> x ≈ 0.0, widths(area)) && return - pa = pixelarea(scene)[] + pa = viewport(scene)[] px_wh = normalize(widths(pa)) wh = normalize(widths(area)) ratio = px_wh ./ wh @@ -89,7 +106,7 @@ function update_cam!(scene::SceneLike, cam::Camera2D) end function correct_ratio!(scene, cam) - on(camera(scene), pixelarea(scene)) do area + on(camera(scene), viewport(scene)) do area neww = widths(area) change = neww .- cam.last_area[] if !(change ≈ Vec(0.0, 0.0)) @@ -123,7 +140,7 @@ function add_pan!(scene::SceneLike, cam::Camera2D) diff = startpos[] .- mp startpos[] = mp area = cam.area[] - diff = Vec(diff) .* wscale(pixelarea(scene)[], area) + diff = Vec(diff) .* wscale(viewport(scene)[], area) cam.area[] = Rectf(minimum(area) .+ diff, widths(area)) update_cam!(scene, cam) active[] = false @@ -141,7 +158,7 @@ function add_pan!(scene::SceneLike, cam::Camera2D) diff = startpos[] .- pos startpos[] = pos area = cam.area[] - diff = Vec(diff) .* wscale(pixelarea(scene)[], area) + diff = Vec(diff) .* wscale(viewport(scene)[], area) cam.area[] = Rectf(minimum(area) .+ diff, widths(area)) update_cam!(scene, cam) return Consume(true) @@ -156,7 +173,7 @@ function add_zoom!(scene::SceneLike, cam::Camera2D) @extractvalue cam (zoomspeed, zoombutton, area) zoom = Float32(x[2]) if zoom != 0 && ispressed(scene, zoombutton) && is_mouseinside(scene) - pa = pixelarea(scene)[] + pa = viewport(scene)[] z = (1f0 - zoomspeed)^zoom mp = Vec2f(e.mouseposition[]) - minimum(pa) mp = (mp .* wscale(pa, area)) + minimum(area) @@ -173,7 +190,7 @@ function add_zoom!(scene::SceneLike, cam::Camera2D) end function camspace(scene::SceneLike, cam::Camera2D, point) - point = Vec(point) .* wscale(pixelarea(scene)[], cam.area[]) + point = Vec(point) .* wscale(viewport(scene)[], cam.area[]) return Vec(point) .+ Vec(minimum(cam.area[])) end @@ -301,6 +318,7 @@ function add_restriction!(cam, window, rarea::Rect2, minwidths::Vec) end struct PixelCamera <: AbstractCamera end +get_space(::PixelCamera) = :pixel struct UpdatePixelCam @@ -308,6 +326,7 @@ struct UpdatePixelCam near::Float32 far::Float32 end +get_space(::UpdatePixelCam) = :pixel function (cam::UpdatePixelCam)(window_size) w, h = Float32.(widths(window_size)) @@ -318,27 +337,31 @@ end """ campixel!(scene; nearclip=-1000f0, farclip=1000f0) -Creates a pixel-level camera for the `Scene`. No controls! +Creates a pixel camera for the given `scene`. This means that the positional +data of a plot will be interpreted in pixel units. This camera does not feature +controls. """ function campixel!(scene::Scene; nearclip=-10_000f0, farclip=10_000f0) disconnect!(camera(scene)) update_once = Observable(false) closure = UpdatePixelCam(camera(scene), nearclip, farclip) - on(closure, camera(scene), pixelarea(scene)) + on(closure, camera(scene), viewport(scene)) cam = PixelCamera() # update once - closure(pixelarea(scene)[]) + closure(viewport(scene)[]) cameracontrols!(scene, cam) update_once[] = true return cam end struct RelativeCamera <: AbstractCamera end +get_space(::RelativeCamera) = :relative """ cam_relative!(scene) -Creates a pixel-level camera for the `Scene`. No controls! +Creates a camera for the given `scene` which maps the scene area to a 0..1 by +0..1 range. This camera does not feature controls. """ function cam_relative!(scene::Scene; nearclip=-10_000f0, farclip=10_000f0) projection = orthographicprojection(0f0, 1f0, 0f0, 1f0, nearclip, farclip) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 2b695aa0043..dc789edadaf 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -1,36 +1,66 @@ -struct Camera3D <: AbstractCamera +abstract type AbstractCamera3D <: AbstractCamera end + +get_space(::AbstractCamera3D) = :data + +struct Camera3D <: AbstractCamera3D + # User settings + settings::Attributes + controls::Attributes + + # Interactivity + pulser::Observable{Float64} + selected::Observable{Bool} + + # view matrix eyeposition::Observable{Vec3f} lookat::Observable{Vec3f} upvector::Observable{Vec3f} - zoom_mult::Observable{Float32} - fov::Observable{Float32} # WGLMakie compat + # perspective projection matrix + fov::Observable{Float32} near::Observable{Float32} far::Observable{Float32} - pulser::Observable{Float64} - - attributes::Attributes + bounding_sphere::Observable{Sphere{Float32}} end """ - Camera3D(scene[; attributes...]) + Camera3D(scene[; kwargs...]) -Creates a 3d camera with a lot of controls. +Sets up a 3D camera with mouse and keyboard controls. -The 3D camera is (or can be) unrestricted in terms of rotations and translations. Both `cam3d!(scene)` and `cam3d_cad!(scene)` create this camera type. Unlike the 2D camera, settings and controls are stored in the `cam.attributes` field rather than in the struct directly, but can still be passed as keyword arguments. The general camera settings include +The behavior of the camera can be adjusted via keyword arguments or the fields +`settings` and `controls`. + +## Settings + +Settings include anything that isn't a mouse or keyboard button. -- `fov = 45f0` sets the "neutral" field of view, i.e. the fov corresponding to no zoom. This is irrelevant if the camera uses an orthographic projection. -- `near = automatic` sets the value of the near clip. By default this will be chosen based on the scenes bounding box. The final value is in `cam.near`. -- `far = automatic` sets the value of the far clip. By default this will be chosen based on the scenes bounding box. The final value is in `cam.far`. -- `rotation_center = :lookat` sets the default center for camera rotations. Currently allows `:lookat` or `:eyeposition`. - `projectiontype = Perspective` sets the type of the projection. Can be `Orthographic` or `Perspective`. -- `fixed_axis = false`: If true panning uses the (world/plot) z-axis instead of the camera up direction. -- `zoom_shift_lookat = true`: If true attempts to keep data under the cursor in view when zooming. +- `rotation_center = :lookat` sets the default center for camera rotations. Currently allows `:lookat` or `:eyeposition`. +- `fixed_axis = true`: If true panning uses the (world/plot) z-axis instead of the camera up direction. +- `zoom_shift_lookat = true`: If true keeps the data under the cursor when zooming. - `cad = false`: If true rotates the view around `lookat` when zooming off-center. +- `clipping_mode = :adaptive`: Controls how `near` and `far` get processed. Options: + - `:static` passes `near` and `far` as is + - `:adaptive` scales `near` by `norm(eyeposition - lookat)` and passes `far` as is + - `:view_relative` scales `near` and `far` by `norm(eyeposition - lookat)` + - `:bbox_relative` scales `near` and `far` to the scene bounding box as passed to the camera with `update_cam!(..., bbox)`. (More specifically `far = 1` is scaled to the furthest point of a bounding sphere and `near` is generally overwritten to be the closest point.) +- `center = true`: Controls whether the camera placement gets reset when calling `update_cam!(scene[, cam], bbox)`, which is called when a new plot is added. This is automatically set to `false` after calling `update_cam!(scene[, cam], eyepos, lookat[, up])`. -The camera view follows from the position of the camera `eyeposition`, the point which the camera focuses `lookat` and the up direction of the camera `upvector`. These can be accessed as `cam.eyeposition` etc and adjusted via `update_cam!(scene, cameracontrols(scene), eyeposition, lookat[, upvector = Vec3f(0, 0, 1)])`. They can also be passed as keyword arguments when the camera is constructed. +- `keyboard_rotationspeed = 1f0` sets the speed of keyboard based rotations. +- `keyboard_translationspeed = 0.5f0` sets the speed of keyboard based translations. +- `keyboard_zoomspeed = 1f0` sets the speed of keyboard based zooms. -The camera can be controlled by keyboard and mouse. The keyboard has the following available attributes +- `mouse_rotationspeed = 1f0` sets the speed of mouse rotations. +- `mouse_translationspeed = 0.5f0` sets the speed of mouse translations. +- `mouse_zoomspeed = 1f0` sets the speed of mouse zooming (mousewheel). + +- `update_rate = 1/30` sets the rate at which keyboard based camera updates are evaluated. +- `circular_rotation = (true, true, true)` enables circular rotations for (fixed x, fixed y, fixed z) rotation axis. (This means drawing a circle with your mouse around the center of the scene will result in a continuous rotation.) + +## Controls + +Controls include any kind of hotkey setting. - `up_key = Keyboard.r` sets the key for translations towards the top of the screen. - `down_key = Keyboard.f` sets the key for translations towards the bottom of the screen. @@ -39,10 +69,10 @@ The camera can be controlled by keyboard and mouse. The keyboard has the followi - `forward_key = Keyboard.w` sets the key for translations into the screen. - `backward_key = Keyboard.s` sets the key for translations out of the screen. -- `zoom_in_key = Keyboard.u` sets the key for zooming into the scene (enlarge, via fov). -- `zoom_out_key = Keyboard.o` sets the key for zooming out of the scene (shrink, via fov). -- `stretch_view_key = Keyboard.page_up` sets the key for moving `eyepostion` away from `lookat`. -- `contract_view_key = Keyboard.page_down` sets the key for moving `eyeposition` towards `lookat`. +- `zoom_in_key = Keyboard.u` sets the key for zooming into the scene (translate eyeposition towards lookat). +- `zoom_out_key = Keyboard.o` sets the key for zooming out of the scene (translate eyeposition away from lookat). +- `increase_fov_key = Keyboard.b` sets the key for increasing the fov. +- `decrease_fov_key = Keyboard.n` sets the key for decreasing the fov. - `pan_left_key = Keyboard.j` sets the key for rotations around the screens vertical axis. - `pan_right_key = Keyboard.l` sets the key for rotations around the screens vertical axis. @@ -51,101 +81,123 @@ The camera can be controlled by keyboard and mouse. The keyboard has the followi - `roll_clockwise_key = Keyboard.e` sets the key for rotations of the screen. - `roll_counterclockwise_key = Keyboard.q` sets the key for rotations of the screen. -- `keyboard_rotationspeed = 1f0` sets the speed of keyboard based rotations. -- `keyboard_translationspeed = 0.5f0` sets the speed of keyboard based translations. -- `keyboard_zoomspeed = 1f0` sets the speed of keyboard based zooms. -- `update_rate = 1/30` sets the rate at which keyboard based camera updates are evaluated. - -and mouse interactions are controlled by +- `fix_x_key = Keyboard.x` sets the key for fixing translations and rotations to the (world/plot) x-axis. +- `fix_y_key = Keyboard.y` sets the key for fixing translations and rotations to the (world/plot) y-axis. +- `fix_z_key = Keyboard.z` sets the key for fixing translations and rotations to the (world/plot) z-axis. +- `reset = Keyboard.left_control & Mouse.left` sets the key for resetting the camera. This equivalent to calling `center!(scene)`. +- `reposition_button = Keyboard.left_alt & Mouse.left` sets the key for focusing the camera on a plot object. - `translation_button = Mouse.right` sets the mouse button for drag-translations. (up/down/left/right) - `scroll_mod = true` sets an additional modifier button for scroll-based zoom. (true being neutral) - `rotation_button = Mouse.left` sets the mouse button for drag-rotations. (pan, tilt) -- `mouse_rotationspeed = 1f0` sets the speed of mouse rotations. -- `mouse_translationspeed = 0.5f0` sets the speed of mouse translations. -- `mouse_zoomspeed = 1f0` sets the speed of mouse zooming (mousewheel). -- `circular_rotation = (true, true, true)` enables circular rotations for (fixed x, fixed y, fixed z) rotation axis. (This means drawing a circle with your mouse around the center of the scene will result in a continuous rotation.) +## Other kwargs -There are also a few generally applicable controls: +Some keyword arguments are used to initialize fields. These include -- `fix_x_key = Keyboard.x` sets the key for fixing translations and rotations to the (world/plot) x-axis. -- `fix_y_key = Keyboard.y` sets the key for fixing translations and rotations to the (world/plot) y-axis. -- `fix_z_key = Keyboard.z` sets the key for fixing translations and rotations to the (world/plot) z-axis. -- `reset = Keyboard.home` sets the key for fully resetting the camera. This equivalent to setting `lookat = Vec3f(0)`, `upvector = Vec3f(0, 0, 1)`, `eyeposition = Vec3f(3)` and then calling `center!(scene)`. +- `eyeposition = Vec3f(3)`: The position of the camera. +- `lookat = Vec3f(0)`: The point the camera is focused on. +- `upvector = Vec3f(0, 0, 1)`: The world direction corresponding to the up direction of the screen. -You can also make adjustments to the camera position, rotation and zoom by calling relevant functions: +- `fov = 45.0` is the field of view. This is irrelevant if the camera uses an orthographic projection. +- `near = automatic` sets the position of the near clip plane. Anything between the camera and the near clip plane is hidden. Must be greater 0. Usage depends on `clipping_mode`. +- `far = automatic` sets the position of the far clip plane. Anything further away than the far clip plane is hidden. Usage depends on `clipping_mode`. Defaults to `1` for `clipping_mode = :bbox_relative`, `2` for `:view_relative` or a value derived from limits for `:static`. + +Note that updating these observables in an active camera requires a call to `update_cam(scene)` +for them to be applied. For updating `eyeposition`, `lookat` and/or upvector +`update_cam!(scene, eyeposition, lookat, upvector = Vec3f(0,0,1))` is preferred. + +The camera position and orientation can also be adjusted via the functions - `translate_cam!(scene, v)` will translate the camera by the given world/plot space vector `v`. - `rotate_cam!(scene, angles)` will rotate the camera around its axes with the corresponding angles. The first angle will rotate around the cameras "right" that is the screens horizontal axis, the second around the up vector/vertical axis or `Vec3f(0, 0, +-1)` if `fixed_axis = true`, and the third will rotate around the view direction i.e. the axis out of the screen. The rotation respects the current `rotation_center` of the camera. - `zoom!(scene, zoom_step)` will change the zoom level of the scene without translating or rotating the scene. `zoom_step` applies multiplicatively to `cam.zoom_mult` which is used as a multiplier to the fov (perspective projection) or width and height (orthographic projection). """ function Camera3D(scene::Scene; kwargs...) - attr = merged_get!(:cam3d, scene, Attributes(kwargs)) do - Attributes( - # Keyboard controls - # Translations - up_key = Keyboard.r, - down_key = Keyboard.f, - left_key = Keyboard.a, - right_key = Keyboard.d, - forward_key = Keyboard.w, - backward_key = Keyboard.s, - # Zooms - zoom_in_key = Keyboard.u, - zoom_out_key = Keyboard.o, - stretch_view_key = Keyboard.page_up, - contract_view_key = Keyboard.page_down, - # Rotations - pan_left_key = Keyboard.j, - pan_right_key = Keyboard.l, - tilt_up_key = Keyboard.i, - tilt_down_key = Keyboard.k, - roll_clockwise_key = Keyboard.e, - roll_counterclockwise_key = Keyboard.q, - # Mouse controls - translation_button = Mouse.right, - scroll_mod = true, - rotation_button = Mouse.left, - # Shared controls - fix_x_key = Keyboard.x, - fix_y_key = Keyboard.y, - fix_z_key = Keyboard.z, - reset = Keyboard.home, - # Settings - keyboard_rotationspeed = 1f0, - keyboard_translationspeed = 0.5f0, - keyboard_zoomspeed = 1f0, - mouse_rotationspeed = 1f0, - mouse_translationspeed = 1f0, - mouse_zoomspeed = 1f0, - circular_rotation = (true, true, true), - fov = 45f0, # base fov - near = automatic, - far = automatic, - rotation_center = :lookat, - update_rate = 1/30, - projectiontype = Perspective, - fixed_axis = true, - zoom_shift_lookat = false, # doesn't really work with fov - cad = false, - # internal - selected = true - ) + overwrites = Attributes(kwargs) + + controls = Attributes( + # Keyboard controls + # Translations + up_key = Keyboard.r, + down_key = Keyboard.f, + left_key = Keyboard.a, + right_key = Keyboard.d, + forward_key = Keyboard.w, + backward_key = Keyboard.s, + # Zooms + zoom_in_key = Keyboard.u, + zoom_out_key = Keyboard.o, + increase_fov_key = Keyboard.b, + decrease_fov_key = Keyboard.n, + # Rotations + pan_left_key = Keyboard.j, + pan_right_key = Keyboard.l, + tilt_up_key = Keyboard.i, + tilt_down_key = Keyboard.k, + roll_clockwise_key = Keyboard.e, + roll_counterclockwise_key = Keyboard.q, + # Mouse controls + translation_button = Mouse.right, + rotation_button = Mouse.left, + scroll_mod = true, + reposition_button = Keyboard.left_alt & Mouse.left, + # Shared controls + fix_x_key = Keyboard.x, + fix_y_key = Keyboard.y, + fix_z_key = Keyboard.z, + reset = Keyboard.left_control & Mouse.left + ) + + replace!(controls, :Camera3D, scene, overwrites) + + settings = Attributes( + keyboard_rotationspeed = 1f0, + keyboard_translationspeed = 0.5f0, + keyboard_zoomspeed = 1f0, + + mouse_rotationspeed = 1f0, + mouse_translationspeed = 1f0, + mouse_zoomspeed = 1f0, + + projectiontype = Makie.Perspective, + circular_rotation = (true, true, true), + rotation_center = :lookat, + update_rate = 1/30, + zoom_shift_lookat = true, + fixed_axis = true, + cad = false, + center = true, + clipping_mode = :adaptive + ) + + replace!(settings, :Camera3D, scene, overwrites) + + if settings.clipping_mode[] === :view_relative + far_default = 2f0 + elseif settings.clipping_mode[] === :bbox_relative + far_default = 1f0 + else + far_default = 100f0 # will be set when inserting a plot end cam = Camera3D( - pop!(attr, :eyeposition, Vec3f(3)), - pop!(attr, :lookat, Vec3f(0)), - pop!(attr, :upvector, Vec3f(0, 0, 1)), - - Observable(1f0), - Observable(attr[:fov][]), - Observable(attr[:near][] === automatic ? 0.1f0 : attr[:near][]), - Observable(attr[:far][] === automatic ? 100f0 : attr[:far][]), - Observable(-1.0), + settings, controls, - attr + # Internals - controls + Observable(-1.0), + Observable(true), + + # Semi-Internal - view matrix + get(overwrites, :eyeposition, Observable(Vec3f(3, 3, 3))), + get(overwrites, :lookat, Observable(Vec3f(0, 0, 0))), + get(overwrites, :upvector, Observable(Vec3f(0, 0, 1))), + + # Semi-Internal - projection matrix + get(overwrites, :fov, Observable(45.0)), + get(overwrites, :near, Observable(0.1)), + get(overwrites, :far, Observable(far_default)), + Sphere(Point3f(0), 1f0) ) disconnect!(camera(scene)) @@ -154,9 +206,9 @@ function Camera3D(scene::Scene; kwargs...) # ticks every so often to get consistent position updates. on(cam.pulser) do prev_time current_time = time() - active = on_pulse(scene, cam, Float32(current_time - prev_time)) - @async if active && attr.selected[] - sleep(attr.update_rate[]) + active = on_pulse(scene, cam, current_time - prev_time) + @async if active && cam.selected[] + sleep(settings.update_rate[]) cam.pulser[] = current_time else cam.pulser.val = -1.0 @@ -165,7 +217,7 @@ function Camera3D(scene::Scene; kwargs...) keynames = ( :up_key, :down_key, :left_key, :right_key, :forward_key, :backward_key, - :zoom_in_key, :zoom_out_key, :stretch_view_key, :contract_view_key, + :zoom_in_key, :zoom_out_key, :increase_fov_key, :decrease_fov_key, :pan_left_key, :pan_right_key, :tilt_up_key, :tilt_down_key, :roll_clockwise_key, :roll_counterclockwise_key ) @@ -173,7 +225,7 @@ function Camera3D(scene::Scene; kwargs...) # Start ticking if relevant keys are pressed 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.selected[] && any(key -> ispressed(scene, controls[key][]), keynames) cam.pulser[] = time() return Consume(true) end @@ -185,37 +237,33 @@ function Camera3D(scene::Scene; kwargs...) deselect_all_cameras!(root(scene)) on(camera(scene), events(scene).mousebutton, priority = 100) do event if event.action == Mouse.press - attr.selected[] = is_mouseinside(scene) + cam.selected[] = is_mouseinside(scene) end return Consume(false) end # Mouse controls - add_translation!(scene, cam) - add_rotation!(scene, cam) + add_mouse_controls!(scene, cam) # add camera controls to scene cameracontrols!(scene, cam) # Trigger updates on scene resize and settings change - on(camera(scene), scene.px_area, attr[:fov], attr[:projectiontype]) do _, _, _ - update_cam!(scene, cam) + on(camera(scene), cam.fov) do _ + if settings.projectiontype[] == Makie.Perspective + update_cam!(scene, cam) + end end - on(camera(scene), attr[:near], attr[:far]) do near, far - near === automatic || (cam.near[] = near) - far === automatic || (cam.far[] = far) + on(camera(scene), scene.viewport, cam.near, cam.far, settings.projectiontype) do _, _, _, _ update_cam!(scene, cam) end # reset on(camera(scene), events(scene).keyboardbutton) do event - if attr.selected[] && event.key == attr[:reset][] && event.action == Keyboard.release + if cam.selected[] && ispressed(scene, controls[:reset][]) # center keeps the rotation of the camera so we reset that here # might make sense to keep user set lookat, upvector, eyeposition # around somewhere for this? - cam.lookat[] = Vec3f(0) - cam.upvector[] = Vec3f(0,0,1) - cam.eyeposition[] = Vec3f(3) center!(scene) return Consume(true) end @@ -226,15 +274,27 @@ function Camera3D(scene::Scene; kwargs...) end # These imitate the old camera +""" + cam3d!(scene[; kwargs...]) + +Creates a `Camera3D` with `zoom_shift_lookat = true` and `fixed_axis = true`. +For more information, see [`Camera3D``](@ref) +""" cam3d!(scene; zoom_shift_lookat = true, fixed_axis = true, kwargs...) = Camera3D(scene, zoom_shift_lookat = zoom_shift_lookat, fixed_axis = fixed_axis; kwargs...) +""" + cam3d_cad!(scene[; kwargs...]) + +Creates a `Camera3D` with `cad = true`, `zoom_shift_lookat = false` and +`fixed_axis = false`. For more information, see [`Camera3D``](@ref) +""" cam3d_cad!(scene; cad = true, zoom_shift_lookat = false, fixed_axis = false, kwargs...) = Camera3D(scene, cad = cad, zoom_shift_lookat = zoom_shift_lookat, fixed_axis = fixed_axis; kwargs...) function deselect_all_cameras!(scene) cam = cameracontrols(scene) - cam isa Camera3D && (cam.attributes.selected[] = false) + cam isa AbstractCamera3D && (cam.selected[] = false) for child in scene.children deselect_all_cameras!(child) end @@ -242,101 +302,187 @@ function deselect_all_cameras!(scene) end -function add_translation!(scene, cam::Camera3D) - translationspeed = cam.attributes[:mouse_translationspeed] - zoomspeed = cam.attributes[:mouse_zoomspeed] - shift_lookat = cam.attributes[:zoom_shift_lookat] - cad = cam.attributes[:cad] - button = cam.attributes[:translation_button] - scroll_mod = cam.attributes[:scroll_mod] +################################################################################ +### Interactivity init +################################################################################ - last_mousepos = RefValue(Vec2f(0, 0)) - dragging = RefValue(false) - function compute_diff(delta) - if cam.attributes[:projectiontype][] == Orthographic - aspect = Float32((/)(widths(scene.px_area[])...)) - aspect_scale = Vec2f(1f0 + aspect, 1f0 + 1f0 / aspect) - return cam.zoom_mult[] * delta .* aspect_scale ./ widths(scene.px_area[]) + +function on_pulse(scene, cam::Camera3D, timestep) + @extractvalue cam.controls ( + right_key, left_key, up_key, down_key, backward_key, forward_key, + tilt_up_key, tilt_down_key, pan_left_key, pan_right_key, roll_counterclockwise_key, roll_clockwise_key, + zoom_out_key, zoom_in_key, increase_fov_key, decrease_fov_key + ) + @extractvalue cam.settings ( + keyboard_translationspeed, keyboard_rotationspeed, keyboard_zoomspeed, projectiontype + ) + + # translation + right = ispressed(scene, right_key) + left = ispressed(scene, left_key) + up = ispressed(scene, up_key) + down = ispressed(scene, down_key) + backward = ispressed(scene, backward_key) + forward = ispressed(scene, forward_key) + translating = right || left || up || down || backward || forward + + if translating + # translation in camera space x/y/z direction + if projectiontype == Perspective + viewnorm = norm(cam.lookat[] - cam.eyeposition[]) + xynorm = 2 * viewnorm * tand(0.5 * cam.fov[]) + translation = keyboard_translationspeed * timestep * Vec3f( + xynorm * (right - left), + xynorm * (up - down), + viewnorm * (backward - forward) + ) else - viewdir = cam.lookat[] - cam.eyeposition[] - return 0.002f0 * cam.zoom_mult[] * norm(viewdir) * delta + # translation in camera space x/y/z direction + viewnorm = norm(cam.eyeposition[] - cam.lookat[]) + translation = 2 * viewnorm * keyboard_translationspeed * timestep * Vec3f( + right - left, up - down, backward - forward + ) end + _translate_cam!(scene, cam, translation) end - # drag start/stop - on(camera(scene), scene.events.mousebutton) do event - if ispressed(scene, button[]) - if event.action == Mouse.press && is_mouseinside(scene) && !dragging[] - last_mousepos[] = mouseposition_px(scene) - dragging[] = true - return Consume(true) - end - elseif event.action == Mouse.release && dragging[] - mousepos = mouseposition_px(scene) - diff = compute_diff(last_mousepos[] .- mousepos) - last_mousepos[] = mousepos - dragging[] = false - translate_cam!(scene, cam, translationspeed[] .* Vec3f(diff[1], diff[2], 0f0)) - return Consume(true) - end - return Consume(false) + # rotation + up = ispressed(scene, tilt_up_key) + down = ispressed(scene, tilt_down_key) + left = ispressed(scene, pan_left_key) + right = ispressed(scene, pan_right_key) + counterclockwise = ispressed(scene, roll_counterclockwise_key) + clockwise = ispressed(scene, roll_clockwise_key) + rotating = up || down || left || right || counterclockwise || clockwise + + if rotating + # rotations around camera space x/y/z axes + angles = keyboard_rotationspeed * timestep * + Vec3f(up - down, left - right, counterclockwise - clockwise) + + _rotate_cam!(scene, cam, angles) end - # in drag - on(camera(scene), scene.events.mouseposition) do mp - if dragging[] && ispressed(scene, button[]) - mousepos = screen_relative(scene, mp) - diff = compute_diff(last_mousepos[] .- mousepos) - last_mousepos[] = mousepos - translate_cam!(scene, cam, translationspeed[] * Vec3f(diff[1], diff[2], 0f0)) - return Consume(true) - end - return Consume(false) + # zoom + zoom_out = ispressed(scene, zoom_out_key) + zoom_in = ispressed(scene, zoom_in_key) + zooming = zoom_out || zoom_in + + if zooming + zoom_step = (1f0 + keyboard_zoomspeed * timestep) ^ (zoom_out - zoom_in) + _zoom!(scene, cam, zoom_step, false, false) end - on(camera(scene), scene.events.scroll) do scroll - if is_mouseinside(scene) && ispressed(scene, scroll_mod[]) - zoom_step = (1f0 + 0.1f0 * zoomspeed[]) ^ -scroll[2] - zoom!(scene, cam, zoom_step, shift_lookat[], cad[]) - return Consume(true) - end - return Consume(false) + # fov + fov_inc = ispressed(scene, increase_fov_key) + fov_dec = ispressed(scene, decrease_fov_key) + fov_adjustment = fov_inc || fov_dec + + if fov_adjustment + step = (1 + keyboard_zoomspeed * timestep) ^ (fov_inc - fov_dec) + cam.fov[] = clamp(cam.fov[] * step, 0.1, 179) + end + + # if any are active, update matrices, else stop clock + if translating || rotating || zooming || fov_adjustment + update_cam!(scene, cam) + return true + else + return false end end -function add_rotation!(scene, cam::Camera3D) - rotationspeed = cam.attributes[:mouse_rotationspeed] - button = cam.attributes[:rotation_button] + +function add_mouse_controls!(scene, cam::Camera3D) + @extract cam.controls (translation_button, rotation_button, reposition_button, scroll_mod) + @extract cam.settings ( + mouse_translationspeed, mouse_rotationspeed, mouse_zoomspeed, + cad, projectiontype, zoom_shift_lookat + ) + last_mousepos = RefValue(Vec2f(0, 0)) - dragging = RefValue(false) + dragging = RefValue((false, false)) # rotation, translation + e = events(scene) + function compute_diff(delta) + if projectiontype[] == Perspective + # TODO wrong scaling? :( + ynorm = 2 * norm(cam.lookat[] - cam.eyeposition[]) * tand(0.5 * cam.fov[]) + return ynorm / size(scene, 2) * delta + else + viewnorm = norm(cam.eyeposition[] - cam.lookat[]) + return 2 * viewnorm / size(scene, 2) * delta + end + end + # drag start/stop on(camera(scene), e.mousebutton) do event - if ispressed(scene, button[]) - if event.action == Mouse.press && is_mouseinside(scene) && !dragging[] + # Drag start translation/rotation + if event.action == Mouse.press && is_mouseinside(scene) + if ispressed(scene, translation_button[]) + last_mousepos[] = mouseposition_px(scene) + dragging[] = (false, true) + return Consume(true) + elseif ispressed(scene, rotation_button[]) last_mousepos[] = mouseposition_px(scene) - dragging[] = true + dragging[] = (true, false) return Consume(true) end - elseif event.action == Mouse.release && dragging[] - mousepos = mouseposition_px(scene) - dragging[] = false - rot_scaling = rotationspeed[] * (e.window_dpi[] * 0.005) - mp = (last_mousepos[] .- mousepos) .* 0.01f0 .* rot_scaling - last_mousepos[] = mousepos - rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) - return Consume(true) + # drag stop & repostion + elseif event.action == Mouse.release + consume = false + + # Drag stop translation/rotation + if dragging[][1] + mousepos = mouseposition_px(scene) + diff = compute_diff(last_mousepos[] .- mousepos) + last_mousepos[] = mousepos + dragging[] = (false, false) + translate_cam!(scene, cam, mouse_translationspeed[] .* Vec3f(diff[1], diff[2], 0f0)) + consume = true + elseif dragging[][2] + mousepos = mouseposition_px(scene) + dragging[] = (false, false) + rot_scaling = mouse_rotationspeed[] * (e.window_dpi[] * 0.005) + mp = (last_mousepos[] .- mousepos) .* 0.01f0 .* rot_scaling + last_mousepos[] = mousepos + rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) + consume = true + end + + # reposition + if ispressed(scene, reposition_button[], event.button) && is_mouseinside(scene) + plt, _, p = ray_assisted_pick(scene) + p3d = to_ndim(Point3f, p, 0f0) + if !isnan(p3d) && to_value(get(plt, :space, :data)) == :data && parent_scene(plt) == scene + # if translation/rotation happens with on-click reposition, + # try uncommenting this + # dragging[] = (false, false) + shift = p3d - cam.lookat[] + update_cam!(scene, cam, cam.eyeposition[] + shift, p3d) + end + consume = true + end + + return Consume(consume) end + return Consume(false) end # in drag on(camera(scene), e.mouseposition) do mp - if dragging[] && ispressed(scene, button[]) + if dragging[][2] && ispressed(scene, translation_button[]) + mousepos = screen_relative(scene, mp) + diff = compute_diff(last_mousepos[] .- mousepos) + last_mousepos[] = mousepos + translate_cam!(scene, cam, mouse_translationspeed[] * Vec3f(diff[1], diff[2], 0f0)) + return Consume(true) + elseif dragging[][1] && ispressed(scene, rotation_button[]) mousepos = screen_relative(scene, mp) - rot_scaling = rotationspeed[] * (e.window_dpi[] * 0.005) + rot_scaling = mouse_rotationspeed[] * (e.window_dpi[] * 0.005) mp = (last_mousepos[] .- mousepos) * 0.01f0 * rot_scaling last_mousepos[] = mousepos rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) @@ -344,80 +490,82 @@ function add_rotation!(scene, cam::Camera3D) end return Consume(false) end -end + #zoom + on(camera(scene), e.scroll) do scroll + if is_mouseinside(scene) && ispressed(scene, scroll_mod[]) + zoom_step = (1f0 + 0.1f0 * mouse_zoomspeed[]) ^ -scroll[2] + zoom!(scene, cam, zoom_step, cad[], zoom_shift_lookat[]) + return Consume(true) + end + return Consume(false) + end -function on_pulse(scene, cam, timestep) - attr = cam.attributes - # translation - right = ispressed(scene, attr[:right_key][]) - left = ispressed(scene, attr[:left_key][]) - up = ispressed(scene, attr[:up_key][]) - down = ispressed(scene, attr[:down_key][]) - backward = ispressed(scene, attr[:backward_key][]) - forward = ispressed(scene, attr[:forward_key][]) - translating = right || left || up || down || backward || forward +end - if translating - # translation in camera space x/y/z direction - translation = attr[:keyboard_translationspeed][] * timestep * - Vec3f(right - left, up - down, backward - forward) - viewdir = cam.lookat[] - cam.eyeposition[] - _translate_cam!(scene, cam, cam.zoom_mult[] * norm(viewdir) * translation) - end - # rotation - up = ispressed(scene, attr[:tilt_up_key][]) - down = ispressed(scene, attr[:tilt_down_key][]) - left = ispressed(scene, attr[:pan_left_key][]) - right = ispressed(scene, attr[:pan_right_key][]) - counterclockwise = ispressed(scene, attr[:roll_counterclockwise_key][]) - clockwise = ispressed(scene, attr[:roll_clockwise_key][]) - rotating = up || down || left || right || counterclockwise || clockwise +################################################################################ +### Camera transformations +################################################################################ - if rotating - # rotations around camera space x/y/z axes - angles = attr[:keyboard_rotationspeed][] * timestep * - Vec3f(up - down, left - right, counterclockwise - clockwise) - _rotate_cam!(scene, cam, angles) - end +# Simplified methods +""" + translate_cam!(scene, cam::Camera3D, v::Vec3) - # zoom - zoom_out = ispressed(scene, attr[:zoom_out_key][]) - zoom_in = ispressed(scene, attr[:zoom_in_key][]) - zooming = zoom_out || zoom_in +Translates the camera by the given vector in camera space, i.e. by `v[1]` to +the right, `v[2]` to the top and `v[3]` forward. - if zooming - zoom_step = (1f0 + attr[:keyboard_zoomspeed][] * timestep) ^ (zoom_out - zoom_in) - _zoom!(scene, cam, zoom_step, false) - end +Note that this method reacts to `fix_x_key` etc. If any of those keys are +pressed the translation will be restricted to act in these directions. +""" +function translate_cam!(scene, cam::Camera3D, t::VecTypes) + _translate_cam!(scene, cam, t) + update_cam!(scene, cam) + nothing +end - stretch = ispressed(scene, attr[:stretch_view_key][]) - contract = ispressed(scene, attr[:contract_view_key][]) - if stretch || contract - zoom_step = (1f0 + attr[:keyboard_zoomspeed][] * timestep) ^ (stretch - contract) - cam.eyeposition[] = cam.lookat[] + zoom_step * (cam.eyeposition[] - cam.lookat[]) - end - zooming = zooming || stretch || contract +""" + rotate_cam!(scene, cam::Camera3D, angles::Vec3) - # if any are active, update matrices, else stop clock - if translating || rotating || zooming - update_cam!(scene, cam) - return true - else - return false - end +Rotates the camera by the given `angles` around the camera x- (left, right), +y- (up, down) and z-axis (in out). The rotation around the y axis is applied +first, then x, then y. + +Note that this method reacts to `fix_x_key` etc and `fixed_axis`. The former +restrict the rotation around a specific axis when a given key is pressed. The +latter keeps the camera y axis fixed as the data space z axis. +""" +function rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) + _rotate_cam!(scene, cam, angles, from_mouse) + update_cam!(scene, cam) + nothing end -function translate_cam!(scene::Scene, cam::Camera3D, t::VecTypes) - _translate_cam!(scene, cam, t) +zoom!(scene, zoom_step) = zoom!(scene, cameracontrols(scene), zoom_step, false, false) +""" + zoom!(scene, cam::Camera3D, zoom_step[, cad = false, zoom_shift_lookat = false]) + +Zooms the camera in or out based on the multiplier `zoom_step`. A `zoom_step` +of 1.0 is neutral, larger zooms out and lower zooms in. + +If `cad = true` zooming will also apply a rotation based on how far the cursor +is from the center of the scene. If `zoom_shift_lookat = true` and +`projectiontype = Orthographic` zooming will keep the data under the cursor at +the same screen space position. +""" +function zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat = false) + _zoom!(scene, cam, zoom_step, cad, zoom_shift_lookat) update_cam!(scene, cam) nothing end -function _translate_cam!(scene, cam, t) + + +function _translate_cam!(scene, cam::Camera3D, t) + @extractvalue cam.controls (fix_x_key, fix_y_key, fix_z_key) + # This uses a camera based coordinate system where # x expands right, y expands up and z expands towards the screen lookat = cam.lookat[] @@ -430,9 +578,9 @@ function _translate_cam!(scene, cam, t) trans = u_x * t[1] + u_y * t[2] + u_z * t[3] # apply world space restrictions - fix_x = ispressed(scene, cam.attributes[:fix_x_key][]) - fix_y = ispressed(scene, cam.attributes[:fix_y_key][]) - fix_z = ispressed(scene, cam.attributes[:fix_z_key][]) + fix_x = ispressed(scene, fix_x_key)::Bool + fix_y = ispressed(scene, fix_y_key)::Bool + fix_z = ispressed(scene, fix_z_key)::Bool if fix_x || fix_y || fix_z trans = Vec3f(fix_x, fix_y, fix_z) .* trans end @@ -443,12 +591,10 @@ function _translate_cam!(scene, cam, t) end -function rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) - _rotate_cam!(scene, cam, angles, from_mouse) - update_cam!(scene, cam) - nothing -end function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) + @extractvalue cam.controls (fix_x_key, fix_y_key, fix_z_key) + @extractvalue cam.settings (fixed_axis, circular_rotation, rotation_center) + # This applies rotations around the x/y/z axis of the camera coordinate system # x expands right, y expands up and z expands towards the screen lookat = cam.lookat[] @@ -458,32 +604,35 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) right = cross(viewdir, up) # +x x_axis = right - y_axis = cam.attributes[:fixed_axis][] ? Vec3f(0, 0, ifelse(up[3] < 0, -1, 1)) : up + y_axis = fixed_axis ? Vec3f(0, 0, ifelse(up[3] < 0, -1, 1)) : up z_axis = -viewdir - fix_x = ispressed(scene, cam.attributes[:fix_x_key][]) - fix_y = ispressed(scene, cam.attributes[:fix_y_key][]) - fix_z = ispressed(scene, cam.attributes[:fix_z_key][]) - cx, cy, cz = cam.attributes[:circular_rotation][] + fix_x = ispressed(scene, fix_x_key)::Bool + fix_y = ispressed(scene, fix_y_key)::Bool + fix_z = ispressed(scene, fix_z_key)::Bool + cx, cy, cz = circular_rotation + rotation = Quaternionf(0, 0, 0, 1) if !xor(fix_x, fix_y, fix_z) # if there are more or less than one restriction apply all rotations + # Note that the y rotation needs to happen first here so that + # fixed_axis = true actually keeps the the axis fixed. rotation *= qrotation(y_axis, angles[2]) rotation *= qrotation(x_axis, angles[1]) rotation *= qrotation(z_axis, angles[3]) else # apply world space restrictions - if from_mouse && ((fix_x && (fix_x == cx)) || (fix_y && (fix_y == cy)) || (fix_z && (fix_z == cz))) + if from_mouse && ((fix_x && cx) || (fix_y && cy) || (fix_z && cz)) # recontextualize the (dy, dx, 0) from mouse rotations so that # drawing circles creates continuous rotations around the fixed axis mp = mouseposition_px(scene) - past_half = 0.5f0 .* widths(scene.px_area[]) .> mp + past_half = 0.5f0 .* size(scene) .> mp flip = 2f0 * past_half .- 1f0 angle = flip[1] * angles[1] + flip[2] * angles[2] - angles = Vec3f(-angle, angle, -angle) + angles = Vec3f(-angle, -angle, angle) # only one fix is true so this only rotates around one axis rotation *= qrotation( - Vec3f(fix_x, fix_z, fix_y) .* Vec3f(sign(right[1]), viewdir[2], sign(up[3])), + Vec3f(fix_x, fix_y, fix_z) .* Vec3f(sign(right[1]), viewdir[2], sign(up[3])), dot(Vec3f(fix_x, fix_y, fix_z), angles) ) else @@ -501,138 +650,175 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) # TODO maybe generalize this to arbitrary center? # calculate positions from rotated vectors - if cam.attributes[:rotation_center][] === :lookat + if rotation_center === :lookat cam.eyeposition[] = lookat - viewdir else cam.lookat[] = eyepos + viewdir end + return end -""" - zoom!(scene, zoom_step) - -Zooms the camera in or out based on the multiplier `zoom_step`. A `zoom_step` -of 1.0 is neutral, larger zooms out and lower zooms in. - -Note that this method only applies to Camera3D. -""" -zoom!(scene::Scene, zoom_step) = zoom!(scene, cameracontrols(scene), zoom_step, false, false) -function zoom!(scene::Scene, cam::Camera3D, zoom_step, shift_lookat = false, cad = false) - _zoom!(scene, cam, zoom_step, shift_lookat, cad) - update_cam!(scene, cam) - nothing -end -function _zoom!(scene::Scene, cam::Camera3D, zoom_step, shift_lookat = false, cad = false) +function _zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat = false) + lookat = cam.lookat[] + eyepos = cam.eyeposition[] + viewdir = lookat - eyepos # -z + vp = viewport(scene)[] + scene_width = widths(vp) if cad - # move exeposition if mouse is not over the center - lookat = cam.lookat[] - eyepos = cam.eyeposition[] - up = cam.upvector[] # +y - viewdir = lookat - eyepos # -z - right = cross(viewdir, up) # +x - - rel_pos = 2f0 * mouseposition_px(scene) ./ widths(scene.px_area[]) .- 1f0 - shift = rel_pos[1] * normalize(right) + rel_pos[2] * normalize(up) - shifted = eyepos + 0.1f0 * sign(1f0 - zoom_step) * norm(viewdir) * shift - cam.eyeposition[] = lookat + norm(viewdir) * normalize(shifted - lookat) - elseif shift_lookat - lookat = cam.lookat[] - eyepos = cam.eyeposition[] - up = normalize(cam.upvector[]) - viewdir = lookat - eyepos - u_z = normalize(-viewdir) - u_x = normalize(cross(up, u_z)) - u_y = normalize(cross(u_z, u_x)) - - if cam.attributes[:projectiontype][] == Perspective - # translate both eyeposition and lookat to more or less keep data - # under the mouse in view - fov = cam.attributes[:fov][] - before = tan(clamp(cam.zoom_mult[] * fov, 0.01f0, 175f0) / 360f0 * Float32(pi)) - after = tan(clamp(cam.zoom_mult[] * zoom_step * fov, 0.01f0, 175f0) / 360f0 * Float32(pi)) - - aspect = Float32((/)(widths(scene.px_area[])...)) - rel_pos = 2f0 * mouseposition_px(scene) ./ widths(scene.px_area[]) .- 1f0 - shift = rel_pos[1] * u_x + rel_pos[2] * u_y - shift = -(after - before) * norm(viewdir) * normalize(aspect .* shift) + # Rotate view based on offset from center + u_z = normalize(viewdir) + u_x = normalize(cross(u_z, cam.upvector[])) + u_y = normalize(cross(u_x, u_z)) + + rel_pos = 2.0f0 * mouseposition_px(scene) ./ scene_width .- 1.0f0 + shift = rel_pos[1] * u_x + rel_pos[2] * u_y + shift *= 0.1 * sign(1 - zoom_step) * norm(viewdir) + + cam.eyeposition[] = lookat - zoom_step * viewdir + shift + elseif zoom_shift_lookat + # keep data under cursor + u_z = normalize(viewdir) + u_x = normalize(cross(u_z, cam.upvector[])) + u_y = normalize(cross(u_x, u_z)) + + rel_pos = (2.0 .* mouseposition_px(scene) .- scene_width) ./ scene_width[2] + shift = (1 - zoom_step) * (rel_pos[1] * u_x + rel_pos[2] * u_y) + + if cam.settings.projectiontype[] == Makie.Orthographic + scale = norm(viewdir) else - mx, my = 2f0 * mouseposition_px(scene) ./ widths(scene.px_area[]) .- 1f0 - aspect = Float32((/)(widths(scene.px_area[])...)) - w = 0.5f0 * (1f0 + aspect) * cam.zoom_mult[] - h = 0.5f0 * (1f0 + 1f0 / aspect) * cam.zoom_mult[] - shift = (1f0 - zoom_step) * (mx * w * u_x + my * h * u_y) + # With perspective projection depth scales shift, but there is no way + # to tell which depth the user may want to keep in view. So we just + # assume it's the same depth as "lookat". + scale = norm(viewdir) * tand(0.5 * cam.fov[]) end - cam.lookat[] = lookat + shift - cam.eyeposition[] = eyepos + shift + cam.lookat[] = lookat + scale * shift + cam.eyeposition[] = lookat - zoom_step * viewdir + scale * shift + else + # just zoom in/out + cam.eyeposition[] = lookat - zoom_step * viewdir end - # apply zoom - cam.zoom_mult[] = cam.zoom_mult[] * zoom_step - return end +################################################################################ +### update_cam! methods +################################################################################ + + +# Update camera matrices function update_cam!(scene::Scene, cam::Camera3D) - @extractvalue cam (lookat, eyeposition, upvector) + @extractvalue cam (lookat, eyeposition, upvector, near, far, fov, bounding_sphere) + + view = Makie.lookat(eyeposition, lookat, upvector) - near = cam.near[]; far = cam.far[] - aspect = Float32((/)(widths(scene.px_area[])...)) + if cam.settings.clipping_mode[] === :view_relative + view_dist = norm(eyeposition - lookat) + near = view_dist * near; far = view_dist * far + elseif cam.settings.clipping_mode[] === :bbox_relative + view_dist = norm(eyeposition - lookat) + center_dist = norm(eyeposition - origin(bounding_sphere)) + far_dist = center_dist + radius(bounding_sphere) + near = max(view_dist * near, center_dist - radius(bounding_sphere)) + far = far_dist * far + elseif cam.settings.clipping_mode[] === :adaptive + view_dist = norm(eyeposition - lookat) + near = view_dist * near; far = far + elseif cam.settings.clipping_mode[] !== :static + @error "clipping_mode = $(cam.settings.clipping_mode[]) not recognized, using :static." + end - if cam.attributes[:projectiontype][] == Perspective - fov = clamp(cam.zoom_mult[] * cam.attributes[:fov][], 0.01f0, 175f0) - cam.fov[] = fov + aspect = Float32((/)(widths(scene)...)) + if cam.settings.projectiontype[] == Makie.Perspective proj = perspectiveprojection(fov, aspect, near, far) else - w = 0.5f0 * (1f0 + aspect) * cam.zoom_mult[] - h = 0.5f0 * (1f0 + 1f0 / aspect) * cam.zoom_mult[] + h = norm(eyeposition - lookat); w = h * aspect proj = orthographicprojection(-w, w, -h, h, near, far) end - view = Makie.lookat(eyeposition, lookat, upvector) - set_proj_view!(camera(scene), proj, view) scene.camera.eyeposition[] = cam.eyeposition[] + scene.camera.lookat[] = cam.lookat[] end -function update_cam!(scene::Scene, camera::Camera3D, area3d::Rect) - @extractvalue camera (lookat, eyeposition, upvector) + +# Update camera position via bbox +function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect, recenter::Bool = cam.settings.center[]) bb = Rect3f(area3d) width = widths(bb) - half_width = width ./ 2f0 - middle = maximum(bb) - half_width - old_dir = normalize(eyeposition .- lookat) - camera.lookat[] = middle - neweyepos = middle .+ (1.2*norm(width) .* old_dir) - camera.eyeposition[] = neweyepos - camera.upvector[] = Vec3f(0,0,1) - if camera.attributes[:near][] === automatic - camera.near[] = 0.1f0 * norm(widths(bb)) + center = maximum(bb) - 0.5f0 * width + radius = 0.5f0 * norm(width) + (isnan(radius) || (radius == 0)) && return + cam.bounding_sphere[] = Sphere(Point3f(center), radius) + + old_dir = normalize(cam.eyeposition[] .- cam.lookat[]) + if cam.settings.projectiontype[] == Makie.Perspective + dist = radius / tand(0.5f0 * cam.fov[]) + else + dist = radius end - if camera.attributes[:far][] === automatic - camera.far[] = 3f0 * norm(widths(bb)) + + if recenter + cam.lookat[] = center + cam.eyeposition[] = cam.lookat[] .+ dist * old_dir + cam.upvector[] = normalize(cross(old_dir, cross(cam.upvector[], old_dir))) end - if camera.attributes[:projectiontype][] == Orthographic - camera.zoom_mult[] = 0.6 * norm(width) - else - camera.zoom_mult[] = 1f0 + + if cam.settings.clipping_mode[] === :static + cam.near[] = 0.1f0 * dist + cam.far[] = 2f0 * dist + elseif cam.settings.clipping_mode[] === :adaptive + cam.near[] = 0.1f0 * dist / norm(cam.eyeposition[] - cam.lookat[]) + cam.far[] = 2f0 * dist end - update_cam!(scene, camera) + + update_cam!(scene, cam) + return end -function update_cam!(scene::Scene, camera::Camera3D, eyeposition, lookat, up = Vec3f(0, 0, 1)) - camera.lookat[] = Vec3f(lookat) +# Update camera position via camera Position & Orientation +function update_cam!(scene::Scene, camera::Camera3D, eyeposition::VecTypes, lookat::VecTypes, up::VecTypes = camera.upvector[]) + camera.settings.center[] = false + camera.lookat[] = Vec3f(lookat) camera.eyeposition[] = Vec3f(eyeposition) - camera.upvector[] = Vec3f(up) + camera.upvector[] = Vec3f(up) + update_cam!(scene, camera) + return +end + +update_cam!(scene::Scene, args::Real...) = update_cam!(scene, cameracontrols(scene), args...) + +""" + update_cam!(scene, cam::Camera3D, ϕ, θ[, radius]) + +Set the camera position based on two angles `0 ≤ ϕ ≤ 2π` and `-pi/2 ≤ θ ≤ pi/2` +and an optional radius around the current `cam.lookat[]`. +""" +function update_cam!( + scene::Scene, camera::Camera3D, phi::Real, theta::Real, + radius::Real = norm(camera.eyeposition[] - camera.lookat[]), + center = camera.lookat[] + ) + camera.settings.center[] = false + st, ct = sincos(theta) + sp, cp = sincos(phi) + v = Vec3f(ct * cp, ct * sp, st) + u = Vec3f(-st * cp, -st * sp, ct) + camera.lookat[] = center + camera.eyeposition[] = center .+ radius * v + camera.upvector[] = u update_cam!(scene, camera) return end + function show_cam(scene) cam = cameracontrols(scene) println("cam=cameracontrols(scene)") diff --git a/src/camera/old_camera3d.jl b/src/camera/old_camera3d.jl index 2d5f424cccf..33d381d2e45 100644 --- a/src/camera/old_camera3d.jl +++ b/src/camera/old_camera3d.jl @@ -46,13 +46,15 @@ function old_cam3d_cad!(scene::Scene; kw_args...) add_translation!(scene, cam, cam.pan_button, cam.move_key, false) add_rotation!(scene, cam, cam.rotate_button, cam.move_key, false) cameracontrols!(scene, cam) - on(camera(scene), scene.px_area) do area + on(camera(scene), scene.viewport) do area # update cam when screen ratio changes update_cam!(scene, cam) end cam end +get_space(::OldCamera3D) = :data + """ old_cam3d_turntable!(scene; kw_args...) @@ -82,7 +84,7 @@ function old_cam3d_turntable!(scene::Scene; kw_args...) add_translation!(scene, cam, cam.pan_button, cam.move_key, true) add_rotation!(scene, cam, cam.rotate_button, cam.move_key, true) cameracontrols!(scene, cam) - on(camera(scene), scene.px_area) do area + on(camera(scene), scene.viewport) do area # update cam when screen ratio changes update_cam!(scene, cam) end @@ -96,7 +98,12 @@ An alias to [`old_cam3d_turntable!`](@ref). Creates a 3D camera for `scene`, which rotates around the plot's axis. """ -const old_cam3d! = old_cam3d_turntable! +old_cam3d!(scene::Scene; kwargs...) = old_cam3d_turntable!(scene; kwargs...) + +@deprecate old_cam3d! cam3d! +@deprecate old_cam3d_turntable! cam3d! +@deprecate old_cam3d_cad! cam3d_cad! + function projection_switch( wh::Rect2, @@ -171,7 +178,7 @@ function add_translation!(scene, cam, key, button, zoom_shift_lookat::Bool) on(camera(scene), scene.events.scroll) do scroll if ispressed(scene, button[]) && is_mouseinside(scene) - cam_res = Vec2f(widths(scene.px_area[])) + cam_res = Vec2f(widths(scene)) mouse_pos_normalized = mouseposition_px(scene) ./ cam_res mouse_pos_normalized = 2*mouse_pos_normalized .- 1f0 zoom_step = scroll[2] @@ -232,7 +239,7 @@ function translate_cam!(scene::Scene, cam::OldCamera3D, _translation::VecTypes) dir = eyeposition - lookat dir_len = norm(dir) - cam_res = Vec2f(widths(scene.px_area[])) + cam_res = Vec2f(widths(scene)) z, x, y = translation z *= 0.1f0 * dir_len @@ -323,7 +330,7 @@ function update_cam!(scene::Scene, cam::OldCamera3D) # TODO this means you can't set FarClip... SAD! # TODO use boundingbox(scene) for optimal far/near far = max(zoom * 5f0, 30f0) - proj = projection_switch(scene.px_area[], fov, near, far, projectiontype, zoom) + proj = projection_switch(scene.viewport[], fov, near, far, projectiontype, zoom) view = Makie.lookat(eyeposition, lookat, upvector) set_proj_view!(camera(scene), proj, view) scene.camera.eyeposition[] = cam.eyeposition[] @@ -351,7 +358,9 @@ end Updates the camera's controls to point to the specified location. """ -update_cam!(scene::Scene, eyeposition, lookat, up = Vec3f(0, 0, 1)) = update_cam!(scene, cameracontrols(scene), eyeposition, lookat, up) +function update_cam!(scene::Scene, eyeposition::VecTypes{3}, lookat::VecTypes{3}, up::VecTypes{3} = Vec3f(0, 0, 1)) + return update_cam!(scene, cameracontrols(scene), eyeposition, lookat, up) +end function update_cam!(scene::Scene, camera::OldCamera3D, eyeposition, lookat, up = Vec3f(0, 0, 1)) camera.lookat[] = Vec3f(lookat) diff --git a/src/camera/projection_math.jl b/src/camera/projection_math.jl index 717224c6a6a..eada9918c2e 100644 --- a/src/camera/projection_math.jl +++ b/src/camera/projection_math.jl @@ -245,7 +245,7 @@ function to_world(scene::Scene, point::T) where T <: StaticVector inv(transformationmatrix(scene)[]) * inv(cam.view[]) * inv(cam.projection[]), - T(widths(pixelarea(scene)[])) + T(size(scene)) ) Point2f(x[1], x[2]) end @@ -280,7 +280,7 @@ end function project(scene::Scene, point::T) where T<:StaticVector cam = scene.camera - area = pixelarea(scene)[] + area = viewport(scene)[] # TODO, I think we need .+ minimum(area) # Which would be semi breaking at this point though, I suppose return project( @@ -344,6 +344,22 @@ function clip_to_space(cam::Camera, space::Symbol) end end +function get_space(scene::Scene) + space = get_space(cameracontrols(scene))::Symbol + space === :data ? (:data,) : (:data, space) +end +get_space(::AbstractCamera) = :data +# TODO: Should this be less specialized? ScenePlot? AbstractPlot? +get_space(plot::Plot) = to_value(get(plot, :space, :data))::Symbol + +is_space_compatible(a, b) = is_space_compatible(get_space(a), get_space(b)) +is_space_compatible(a::Symbol, b::Symbol) = a === b +is_space_compatible(a::Symbol, b::Union{Tuple, Vector}) = a in b +function is_space_compatible(a::Union{Tuple, Vector}, b::Union{Tuple, Vector}) + any(x -> is_space_compatible(x, b), a) +end +is_space_compatible(a::Union{Tuple, Vector}, b::Symbol) = is_space_compatible(b, a) + function project(cam::Camera, input_space::Symbol, output_space::Symbol, pos) input_space === output_space && return to_ndim(Point3f, pos, 0) clip_from_input = space_to_clip(cam, input_space) diff --git a/src/colorsampler.jl b/src/colorsampler.jl index 99deb026186..a8800b46b69 100644 --- a/src/colorsampler.jl +++ b/src/colorsampler.jl @@ -244,13 +244,19 @@ colormapping_type(@nospecialize(colormap)) = continuous colormapping_type(::PlotUtils.CategoricalColorGradient) = banded colormapping_type(::Categorical) = categorical -function ColorMapping( - color::AbstractArray{<:Number, N}, colors_obs, colormap, colorrange, - colorscale, alpha, lowclip, highclip, nan_color, - color_mapping_type=lift(colormapping_type, colormap; ignore_equal_values=true)) where {N} - T = _array_value_type(color) - color_tight = convert(Observable{T}, colors_obs) +function _colormapping( + color_tight::Observable{V}, + @nospecialize(colors_obs), + @nospecialize(colormap), + @nospecialize(colorrange), + @nospecialize(colorscale), + @nospecialize(alpha), + @nospecialize(lowclip), + @nospecialize(highclip), + @nospecialize(nan_color), + color_mapping_type) where {V <: AbstractArray{T, N}} where {N, T} + map_colors = Observable(RGBAf[]; ignore_equal_values=true) raw_colormap = Observable(RGBAf[]; ignore_equal_values=true) mapping = Observable{Union{Nothing,Vector{Float64}}}(nothing; ignore_equal_values=true) @@ -276,7 +282,7 @@ function ColorMapping( _lowclip = Observable{Union{Automatic,RGBAf}}(automatic; ignore_equal_values=true) on(lowclip; update=true) do lc - _lowclip[] = lc isa Union{Nothing, Automatic} ? automatic : to_color(lc) + _lowclip[] = lc isa Union{Nothing,Automatic} ? automatic : to_color(lc) return end _highclip = Observable{Union{Automatic,RGBAf}}(automatic; ignore_equal_values=true) @@ -296,21 +302,38 @@ function ColorMapping( color_scaled = lift(color_tight, colorscale) do color, scale return el32convert(apply_scale(scale, color)) end - CT = ColorMapping{N,T,typeof(color_scaled[])} - - return CT( - color_tight, - map_colors, - raw_colormap, - colorscale, - mapping, - colorrange, - _lowclip, - _highclip, - lift(to_color, nan_color), - color_mapping_type, - colorrange_scaled, - color_scaled) + CT = ColorMapping{N,V,typeof(color_scaled[])} + + return CT(color_tight, + map_colors, + raw_colormap, + colorscale, + mapping, + colorrange, + _lowclip, + _highclip, + lift(to_color, nan_color), + color_mapping_type, + colorrange_scaled, + color_scaled) +end + +function ColorMapping( + color::AbstractArray{<:Number, N}, + @nospecialize(colors_obs), + @nospecialize(colormap), + @nospecialize(colorrange), + @nospecialize(colorscale), + @nospecialize(alpha), + @nospecialize(lowclip), + @nospecialize(highclip), + @nospecialize(nan_color), + color_mapping_type=lift(colormapping_type, colormap; ignore_equal_values=true)) where {N} + + T = _array_value_type(color) + color_tight = convert(Observable{T}, colors_obs)::Observable{T} + _colormapping(color_tight, colors_obs, colormap, colorrange, + colorscale, alpha, lowclip, highclip, nan_color, color_mapping_type) end function assemble_colors(c::AbstractArray{<:Number}, @nospecialize(color), @nospecialize(plot)) diff --git a/src/conversions.jl b/src/conversions.jl index 6fb0d945499..0c947997ce2 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -5,7 +5,7 @@ const RangeLike = Union{AbstractRange, AbstractVector, ClosedInterval} # if no plot type based conversion is defined, we try using a trait function convert_arguments(T::PlotFunc, args...; kw...) - ct = conversion_trait(T) + ct = conversion_trait(T, args...) try convert_arguments(ct, args...; kw...) catch e @@ -41,7 +41,7 @@ end # and reconvert the whole tuple in order to handle missings centrally, e.g. function convert_arguments_individually(T::PlotFunc, args...) # convert each single argument until it doesn't change type anymore - single_converted = recursively_convert_argument.(args) + single_converted = map(recursively_convert_argument, args) # if the type of args hasn't changed this function call didn't help and we error if typeof(single_converted) == typeof(args) throw(MethodError(convert_arguments, (T, args...))) @@ -54,9 +54,9 @@ end function recursively_convert_argument(x) newx = convert_single_argument(x) if typeof(newx) == typeof(x) - x + return x else - recursively_convert_argument(newx) + return recursively_convert_argument(newx) end end @@ -109,7 +109,7 @@ end Enables to use scatter like a surface plot with x::Vector, y::Vector, z::Matrix spanning z over the grid spanned by x y """ -function convert_arguments(::PointBased, x::AbstractArray, y::AbstractVector, z::AbstractMatrix) +function convert_arguments(::PointBased, x::AbstractArray, y::AbstractVector, z::AbstractArray) (vec(Point3f.(x, y', z)),) end @@ -163,7 +163,6 @@ from `x` and `y`. `P` is the plot Type (it is optional). """ -#convert_arguments(::PointBased, x::RealVector, y::RealVector) = (Point2f.(x, y),) convert_arguments(P::PointBased, x::ClosedInterval, y::RealVector) = convert_arguments(P, LinRange(extrema(x)..., length(y)), y) convert_arguments(P::PointBased, x::RealVector, y::ClosedInterval) = convert_arguments(P, x, LinRange(extrema(y)..., length(x))) @@ -311,7 +310,7 @@ end ################################################################################ -# SurfaceLike # +# GridBased # ################################################################################ function edges(v::AbstractVector) @@ -329,62 +328,63 @@ function edges(v::AbstractVector) end end -function adjust_axes(::DiscreteSurface, x::AbstractVector{<:Number}, y::AbstractVector{<:Number}, z::AbstractMatrix) +function adjust_axes(::CellGrid, x::AbstractVector{<:Number}, y::AbstractVector{<:Number}, z::AbstractMatrix) x̂, ŷ = map((x, y), size(z)) do v, sz return length(v) == sz ? edges(v) : v end return x̂, ŷ, z end -adjust_axes(::SurfaceLike, x, y, z) = x, y, z +adjust_axes(::VertexGrid, x, y, z) = x, y, z """ - convert_arguments(SL::SurfaceLike, x::VecOrMat, y::VecOrMat, z::Matrix) + convert_arguments(ct::GridBased, x::VecOrMat, y::VecOrMat, z::Matrix) -If `SL` is `Heatmap` and `x` and `y` are vectors, infer from length of `x` and `y` +If `ct` is `Heatmap` and `x` and `y` are vectors, infer from length of `x` and `y` whether they represent edges or centers of the heatmap bins. If they are centers, convert to edges. Convert eltypes to `Float32` and return outputs as a `Tuple`. """ -function convert_arguments(SL::SurfaceLike, x::AbstractVecOrMat{<: Number}, y::AbstractVecOrMat{<: Number}, z::AbstractMatrix{<: Union{Number, Colorant}}) - return map(el32convert, adjust_axes(SL, x, y, z)) +function convert_arguments(ct::GridBased, x::AbstractVecOrMat{<: Number}, y::AbstractVecOrMat{<: Number}, z::AbstractMatrix{<: Union{Number, Colorant}}) + return map(el32convert, adjust_axes(ct, x, y, z)) end -function convert_arguments(SL::SurfaceLike, x::AbstractVecOrMat{<: Number}, y::AbstractVecOrMat{<: Number}, z::AbstractMatrix{<:Number}) - return map(el32convert, adjust_axes(SL, x, y, z)) +function convert_arguments(ct::GridBased, x::AbstractVecOrMat{<: Number}, y::AbstractVecOrMat{<: Number}, z::AbstractMatrix{<:Number}) + return map(el32convert, adjust_axes(ct, x, y, z)) end -convert_arguments(sl::SurfaceLike, x::AbstractMatrix, y::AbstractMatrix) = convert_arguments(sl, x, y, zeros(size(y))) +convert_arguments(ct::VertexGrid, x::AbstractMatrix, y::AbstractMatrix) = convert_arguments(ct, x, y, zeros(size(y))) """ - convert_arguments(P, x, y, z)::Tuple{ClosedInterval, ClosedInterval, Matrix} + convert_arguments(P, x::RangeLike, y::RangeLike, z::AbstractMatrix) -Takes 2 ClosedIntervals's `x`, `y`, and an AbstractMatrix `z`, and converts the closed range to -linspaces with size(z, 1/2) -`P` is the plot Type (it is optional). +Takes one or two ClosedIntervals `x` and `y` and converts them to closed ranges +with size(z, 1/2). """ -function convert_arguments(P::SurfaceLike, x::ClosedInterval, y::ClosedInterval, z::AbstractMatrix) +function convert_arguments(P::GridBased, x::RangeLike, y::RangeLike, z::AbstractMatrix) convert_arguments(P, to_linspace(x, size(z, 1)), to_linspace(y, size(z, 2)), z) end """ - convert_arguments(P, Matrix)::Tuple{ClosedInterval, ClosedInterval, Matrix} - -Takes an `AbstractMatrix`, converts the dimesions `n` and `m` into `ClosedInterval`, -and stores the `ClosedInterval` to `n` and `m`, plus the original matrix in a Tuple. + convert_arguments(::ImageLike, mat::AbstractMatrix) -`P` is the plot Type (it is optional). +Generates `ClosedInterval`s of size `0 .. size(mat, 1/2)` as x and y values. """ -function convert_arguments(sl::SurfaceLike, data::AbstractMatrix) +function convert_arguments(::ImageLike, data::AbstractMatrix) n, m = Float32.(size(data)) - convert_arguments(sl, 0f0 .. n, 0f0 .. m, el32convert(data)) + return (0f0 .. n, 0f0 .. m, el32convert(data)) +end +function convert_arguments(::ImageLike, xs::RangeLike, ys::RangeLike, data::AbstractMatrix) + x = Float32(minimum(xs)) .. Float32(maximum(xs)) + y = Float32(minimum(ys)) .. Float32(maximum(ys)) + return (x, y, el32convert(data)) end -function convert_arguments(ds::DiscreteSurface, data::AbstractMatrix) +function convert_arguments(ct::GridBased, data::AbstractMatrix) n, m = Float32.(size(data)) - convert_arguments(ds, edges(1:n), edges(1:m), el32convert(data)) + convert_arguments(ct, 1f0 .. n, 1f0 .. m, el32convert(data)) end -function convert_arguments(SL::SurfaceLike, x::AbstractVector{<:Number}, y::AbstractVector{<:Number}, z::AbstractVector{<:Number}) +function convert_arguments(ct::GridBased, x::AbstractVector{<:Number}, y::AbstractVector{<:Number}, z::AbstractVector{<:Number}) if !(length(x) == length(y) == length(z)) error("x, y and z need to have the same length. Lengths are $(length.((x, y, z)))") end @@ -406,7 +406,7 @@ function convert_arguments(SL::SurfaceLike, x::AbstractVector{<:Number}, y::Abst j = searchsortedfirst(y_centers, yi) @inbounds zs[i, j] = zi end - convert_arguments(SL, x_centers, y_centers, zs) + convert_arguments(ct, x_centers, y_centers, zs) end @@ -417,14 +417,14 @@ Takes vectors `x` and `y` and the function `f`, and applies `f` on the grid that This is equivalent to `f.(x, y')`. `P` is the plot Type (it is optional). """ -function convert_arguments(sl::SurfaceLike, x::AbstractVector{T1}, y::AbstractVector{T2}, f::Function) where {T1, T2} +function convert_arguments(ct::Union{GridBased, ImageLike}, x::AbstractVector{T1}, y::AbstractVector{T2}, f::Function) where {T1, T2} if !applicable(f, x[1], y[1]) error("You need to pass a function with signature f(x::$T1, y::$T2). Found: $f") end T = typeof(f(x[1], y[1])) z = similar(x, T, (length(x), length(y))) z .= f.(x, y') - return convert_arguments(sl, x, y, z) + return convert_arguments(ct, x, y, z) end ################################################################################ @@ -458,26 +458,6 @@ function convert_arguments(::VolumeLike, x::AbstractVector, y::AbstractVector, z (x, y, z, el32convert(i)) end - -""" - convert_arguments(P, x, y, z, f)::(Vector, Vector, Vector, Matrix) - -Takes `AbstractVector` `x`, `y`, and `z` and the function `f`, evaluates `f` on the volume -spanned by `x`, `y` and `z`, and puts `x`, `y`, `z` and `f(x,y,z)` in a Tuple. - -`P` is the plot Type (it is optional). -""" -function convert_arguments(::VolumeLike, x::AbstractVector, y::AbstractVector, z::AbstractVector, f::Function) - if !applicable(f, x[1], y[1], z[1]) - error("You need to pass a function with signature f(x, y, z). Found: $f") - end - _x, _y, _z = ntuple(Val(3)) do i - A = (x, y, z)[i] - reshape(A, ntuple(j-> j != i ? 1 : length(A), Val(3))) - end - return (x, y, z, el32convert.(f.(_x, _y, _z))) -end - ################################################################################ # <:Lines # ################################################################################ @@ -613,44 +593,69 @@ function convert_arguments( vertices::AbstractArray, indices::AbstractArray ) - m = normal_mesh(to_vertices(vertices), to_triangles(indices)) - (m,) + vs = to_vertices(vertices) + fs = to_triangles(indices) + if eltype(vs) <: Point{3} + ns = normals(vs, fs) + m = GeometryBasics.Mesh(meta(vs; normals=ns), fs) + else + # TODO, we don't need to add normals here, but maybe nice for type stability? + m = GeometryBasics.Mesh(meta(vs; normals=fill(Vec3f(0, 0, 1), length(vs))), fs) + end + return (m,) end + ################################################################################ -# <:Arrows # +# Function Conversions # ################################################################################ # Allow the user to pass a function to `arrows` which determines the direction # and magnitude of the arrows. The function must accept `Point2f` as input. # and return Point2f or Vec2f or some array like structure as output. -function convert_arguments(::Type{<: Arrows}, x::AbstractVector, y::AbstractVector, f::Function) +function convert_arguments(::Type{<:Arrows}, x::AbstractVector, y::AbstractVector, f::Function) points = Point2f.(x, y') f_out = Vec2f.(f.(points)) return (vec(points), vec(f_out)) end -function convert_arguments(::Type{<: Arrows}, x::AbstractVector, y::AbstractVector, z::AbstractVector, f::Function) +function convert_arguments(::Type{<:Arrows}, x::AbstractVector, y::AbstractVector, z::AbstractVector, + f::Function) points = [Point3f(x, y, z) for x in x, y in y, z in z] f_out = Vec3f.(f.(points)) return (vec(points), vec(f_out)) end -################################################################################ -# Function Conversions # -################################################################################ +""" + convert_arguments(P, x, y, z, f)::(Vector, Vector, Vector, Matrix) + +Takes `AbstractVector` `x`, `y`, and `z` and the function `f`, evaluates `f` on the volume +spanned by `x`, `y` and `z`, and puts `x`, `y`, `z` and `f(x,y,z)` in a Tuple. + +`P` is the plot Type (it is optional). +""" +function convert_arguments(::VolumeLike, x::AbstractVector, y::AbstractVector, z::AbstractVector, f::Function) + if !applicable(f, x[1], y[1], z[1]) + error("You need to pass a function with signature f(x, y, z). Found: $f") + end + _x, _y, _z = ntuple(Val(3)) do i + A = (x, y, z)[i] + return reshape(A, ntuple(j -> j != i ? 1 : length(A), Val(3))) + end + return (x, y, z, el32convert.(f.(_x, _y, _z))) +end function convert_arguments(P::PlotFunc, r::AbstractVector, f::Function) - ptype = plottype(P, Lines) - to_plotspec(ptype, convert_arguments(ptype, r, f.(r))) + return convert_arguments(P, r, map(f, r)) end function convert_arguments(P::PlotFunc, i::AbstractInterval, f::Function) x, y = PlotUtils.adapted_grid(f, endpoints(i)) - ptype = plottype(P, Lines) - to_plotspec(ptype, convert_arguments(ptype, x, y)) + return convert_arguments(P, x, y) end + + # The following `tryrange` code was copied from Plots.jl # https://github.com/MakieOrg/Plots.jl/blob/15dc61feb57cba1df524ce5d69f68c2c4ea5b942/src/series.jl#L399-L416 @@ -673,8 +678,9 @@ function tryrange(F, vec) error("$F is not a Function, or is not defined at any of the values $vec") end + # OffsetArrays conversions -function convert_arguments(sl::SurfaceLike, wm::OffsetArray) +function convert_arguments(sl::GridBased, wm::OffsetArray) x1, y1 = wm.offsets .+ 1 nx, ny = size(wm) x = range(x1, length = nx) @@ -755,12 +761,12 @@ Converts a representation of vertices `v` to its canonical representation as a - otherwise if `v` has 2 or 3 columns, it will treat each row as a vertex. """ function to_vertices(verts::AbstractVector{<: VecTypes{3, T}}) where T - vert3f0 = T != Float32 ? Point3f.(verts) : verts + vert3f0 = T != Float32 ? map(Point3f, verts) : verts return reinterpret(Point3f, vert3f0) end -function to_vertices(verts::AbstractVector{<: VecTypes}) - to_vertices(to_ndim.(Point3f, verts, 0.0)) +function to_vertices(verts::AbstractVector{<: VecTypes{N}}) where {N} + return map(Point{N, Float32}, verts) end function to_vertices(verts::AbstractMatrix{<: Number}) @@ -780,7 +786,7 @@ function to_vertices(verts::AbstractMatrix{T}, ::Val{1}) where T <: Number else let N = Val(N), lverts = verts broadcast(1:size(verts, 2), N) do vidx, n - to_ndim(Point3f, ntuple(i-> lverts[i, vidx], n), 0.0) + Point(ntuple(i-> Float32(lverts[i, vidx]), n)) end end end @@ -789,7 +795,7 @@ end function to_vertices(verts::AbstractMatrix{T}, ::Val{2}) where T <: Number let N = Val(size(verts, 2)), lverts = verts broadcast(1:size(verts, 1), N) do vidx, n - to_ndim(Point3f, ntuple(i-> lverts[vidx, i], n), 0.0) + Point(ntuple(i-> Float32(verts[vidx, i]), n)) end end end @@ -1413,6 +1419,7 @@ to_spritemarker(x::Rect) = x to_spritemarker(b::BezierPath) = b to_spritemarker(b::Polygon) = BezierPath(b) to_spritemarker(b) = error("Not a valid scatter marker: $(typeof(b))") +to_spritemarker(x::Shape) = x function to_spritemarker(str::String) error("Using strings for multiple char markers is deprecated. Use `collect(string)` or `['x', 'o', ...]` instead. Found: $(str)") @@ -1469,6 +1476,8 @@ end convert_attribute(value, ::key"diffuse") = Vec3f(value) convert_attribute(value, ::key"specular") = Vec3f(value) +convert_attribute(value, ::key"backlight") = Float32(value) + # SAMPLER overloads diff --git a/src/deprecated.jl b/src/deprecated.jl index 5bc7fe2b141..37ce9d32922 100644 --- a/src/deprecated.jl +++ b/src/deprecated.jl @@ -1,9 +1,16 @@ -function register_backend!(backend) - @warn("`register_backend!` is an internal deprecated function, which shouldn't be used outside Makie. - if you must really use this function, it's now `set_active_backend!(::Module)") -end +########################################### +# v0.20 deprecations: +## +Base.@deprecate_binding DiscreteSurface CellGrid true +Base.@deprecate_binding ContinuousSurface VertexGrid true -function backend_display(args...) - @warn("`backend_display` is an internal deprecated function, which shouldn't be used outside Makie. - if you must really use this function, it's now just `display(::Backend.Screen, figlike)`") +function Base.getproperty(scene::Scene, field::Symbol) + if field === :px_area + @warn "`.px_area` got renamed to `.viewport`, and means the area the scene maps to in device indepentent units, not pixels. Note, `size(scene) == widths(scene.viewport[])`" + return scene.viewport + end + return getfield(scene, field) end + +@deprecate pixelarea viewport true +Base.@deprecate_binding Combined Plot true diff --git a/src/display.jl b/src/display.jl index 30b7c10ef74..5fc3b60903b 100644 --- a/src/display.jl +++ b/src/display.jl @@ -66,14 +66,13 @@ function set_screen_config!(backend::Module, new_values) return backend_defaults end -function merge_screen_config(::Type{Config}, screen_config_kw) where Config +function merge_screen_config(::Type{Config}, config::Dict) where Config backend = parentmodule(Config) key = nameof(backend) backend_defaults = CURRENT_DEFAULT_THEME[key] - kw_nt = values(screen_config_kw) arguments = map(fieldnames(Config)) do name - if haskey(kw_nt, name) - return getfield(kw_nt, name) + if haskey(config, name) + return config[name] else return to_value(backend_defaults[name]) end @@ -110,7 +109,7 @@ end can_show_inline(::Missing) = false # no backend function can_show_inline(Backend) - for mime in [MIME"juliavscode/html"(), MIME"text/html"(), MIME"image/png"(), MIME"image/svg+xml"()] + for mime in (MIME"juliavscode/html"(), MIME"text/html"(), MIME"image/png"(), MIME"image/svg+xml"()) if backend_showable(Backend.Screen, mime) return has_mime_display(mime) end @@ -130,6 +129,7 @@ see `?Backend.Screen` or `Base.doc(Backend.Screen)` for applicable options. """ function Base.display(figlike::FigureLike; backend=current_backend(), inline=ALWAYS_INLINE_PLOTS[], update = true, screen_config...) + config = Dict{Symbol, Any}(screen_config) if ismissing(backend) error(""" No backend available! @@ -141,10 +141,15 @@ function Base.display(figlike::FigureLike; backend=current_backend(), end # We show inline if explicitely requested or if automatic and we can actually show something inline! + scene = get_scene(figlike) if (inline === true || inline === automatic) && can_show_inline(backend) + # We can't forward the screenconfig to show, but show uses the current screen if there is any + # We use that, to create a screen before show and rely on show picking up that screen + screen = getscreen(backend, scene, config) + push_screen!(scene, screen) Core.invoke(display, Tuple{Any}, figlike) # In WGLMakie, we need to wait for the display being done - screen = getscreen(get_scene(figlike)) + screen = getscreen(scene) wait_for_display(screen) return screen else @@ -156,9 +161,8 @@ function Base.display(figlike::FigureLike; backend=current_backend(), If this wasn't set on purpose, call `Makie.inline!()` to restore the default. """ end - scene = get_scene(figlike) update && update_state_before_display!(figlike) - screen = getscreen(backend, scene; screen_config...) + screen = getscreen(backend, scene, config) display(screen, scene) return screen end @@ -204,9 +208,16 @@ end Base.showable(mime::MIME, fig::FigureLike) = _backend_showable(mime) -# need to define this to resolve ambiguoity issue +# need to define this to resolve ambiguity issue Base.showable(mime::MIME"application/json", fig::FigureLike) = _backend_showable(mime) +const WEB_MIMES = ( + MIME"text/html", + MIME"application/vnd.webio.application+html", + MIME"application/prs.juno.plotpane+html", + MIME"juliavscode/html") + + backend_showable(@nospecialize(screen), @nospecialize(mime)) = false # fallback show when no backend is selected @@ -244,7 +255,7 @@ function Base.show(io::IO, m::MIME, figlike::FigureLike) backend = current_backend() # get current screen the scene is already displayed on, or create a new screen update_state_before_display!(figlike) - screen = getscreen(backend, scene, io, m; visible=false) + screen = getscreen(backend, scene, Dict(:visible=>false), io, m) backend_show(screen, io, m, scene) return screen end @@ -263,7 +274,7 @@ filetype(::FileIO.File{F}) where F = F # Allow format to be overridden with first argument """ - FileIO.save(filename, scene; resolution = size(scene), pt_per_unit = 0.75, px_per_unit = 1.0) + FileIO.save(filename, scene; size = size(scene), pt_per_unit = 0.75, px_per_unit = 1.0) Save a `Scene` with the specified filename and format. @@ -277,7 +288,7 @@ Save a `Scene` with the specified filename and format. ## All Backends -- `resolution`: `(width::Int, height::Int)` of the scene in dimensionless units (equivalent to `px` for GLMakie and WGLMakie). +- `size`: `(width::Int, height::Int)` of the scene in dimensionless units. - `update`: Whether the figure should be updated before saving. This resets the limits of all Axes in the figure. Defaults to `true`. - `backend`: Specify the `Makie` backend that should be used for saving. Defaults to the current backend. - Further keywords will be forwarded to the screen. @@ -296,14 +307,19 @@ end function FileIO.save( file::FileIO.Formatted, fig::FigureLike; - resolution = size(get_scene(fig)), + size = Base.size(get_scene(fig)), + resolution = nothing, backend = current_backend(), update = true, screen_config... ) scene = get_scene(fig) - if resolution != size(scene) - resize!(scene, resolution) + if resolution !== nothing + @warn "The keyword argument `resolution` for `save()` has been deprecated. Use `size` instead, which better reflects that this is a unitless size and not a pixel resolution." + size = resolution + end + if size != Base.size(scene) + resize!(scene, size) end filename = FileIO.filename(file) # Delete previous file if it exists and query only the file string for type. @@ -315,13 +331,16 @@ function FileIO.save( # query the filetype only from the file extension F = filetype(file) mime = format2mime(F) + try return open(filename, "w") do io # If the scene already got displayed, we get the current screen its displayed on # Else, we create a new scene and update the state of the fig update && update_state_before_display!(fig) - visible = !isnothing(getscreen(scene)) # if already has a screen, don't hide it! - screen = getscreen(backend, scene, io, mime; visible=visible, screen_config...) + visible = isvisible(getscreen(scene)) # if already has a screen, don't hide it! + config = Dict{Symbol, Any}(screen_config) + get!(config, :visible, visible) + screen = getscreen(backend, scene, config, io, mime) backend_show(screen, io, mime, scene) end catch e @@ -388,9 +407,9 @@ end getscreen(scene::SceneLike, backend=current_backend()) = getscreen(get_scene(scene), backend) -function getscreen(backend::Union{Missing, Module}, scene::Scene, args...; screen_config...) +function getscreen(backend::Union{Missing, Module}, scene::Scene, _config::Dict, args...) screen = getscreen(scene, backend) - config = Makie.merge_screen_config(backend.ScreenConfig, screen_config) + config = merge_screen_config(backend.ScreenConfig, _config) if !isnothing(screen) && parentmodule(typeof(screen)) == backend new_screen = apply_screen_config!(screen, config, scene, args...) if new_screen !== screen @@ -410,13 +429,17 @@ function getscreen(backend::Union{Missing, Module}, scene::Scene, args...; scree end function get_sub_picture(image, format::ImageStorageFormat, rect) - xmin, ymin = minimum(rect) .- (1, 0) + xmin, ymin = minimum(rect) .- Vec(1, 0) xmax, ymax = maximum(rect) start = size(image, 1) - ymax stop = size(image, 1) - ymin return image[start:stop, xmin:xmax] end +# Needs to be overloaded by backends, with fallback true +isvisible(screen::MakieScreen) = true +isvisible(::Nothing) = false + """ colorbuffer(scene, format::ImageStorageFormat = JuliaNative; update=true, backend=current_backend(), screen_config...) @@ -434,25 +457,38 @@ or RGBA. function colorbuffer(fig::FigureLike, format::ImageStorageFormat = JuliaNative; update=true, backend = current_backend(), screen_config...) scene = get_scene(fig) update && update_state_before_display!(fig) - visible = !isnothing(getscreen(scene)) # if already has a screen, don't hide it! - screen = getscreen(backend, scene; start_renderloop=false, visible=visible, screen_config...) + # if already has a screen, use their visibility value, if no screen, returns false + visible = isvisible(getscreen(scene)) + config = Dict{Symbol,Any}(screen_config) + get!(config, :visible, visible) + get!(config, :start_renderloop, false) + screen = getscreen(backend, scene, config) img = colorbuffer(screen, format) if !isroot(scene) - return get_sub_picture(img, format, pixelarea(scene)[]) + return get_sub_picture(img, format, viewport(scene)[]) else return img end end # Fallback for any backend that will just use colorbuffer to write out an image -function backend_show(screen::MakieScreen, io::IO, m::MIME"image/png", scene::Scene) +function backend_show(screen::MakieScreen, io::IO, ::MIME"image/png", scene::Scene) img = colorbuffer(screen) FileIO.save(FileIO.Stream{FileIO.format"PNG"}(Makie.raw_io(io)), img) return end -function backend_show(screen::MakieScreen, io::IO, m::MIME"image/jpeg", scene::Scene) - img = colorbuffer(scene) +function backend_show(screen::MakieScreen, io::IO, ::MIME"image/jpeg", scene::Scene) + img = colorbuffer(screen) FileIO.save(FileIO.Stream{FileIO.format"JPEG"}(Makie.raw_io(io)), img) return end + +function backend_show(screen::MakieScreen, io::IO, ::Union{WEB_MIMES...}, scene::Scene) + w, h = size(scene) + png_io = IOBuffer() + backend_show(screen, png_io, MIME"image/png"(), scene) + b64 = Base64.base64encode(String(take!(png_io))) + print(io, "") + return +end diff --git a/src/documentation/documentation.jl b/src/documentation/documentation.jl index ce83835cd4c..31001416569 100644 --- a/src/documentation/documentation.jl +++ b/src/documentation/documentation.jl @@ -186,7 +186,7 @@ to_func(func::Function) = func Maps the input of a function name to its cooresponding Type. """ -to_type(func::Function) = Combined{func} +to_type(func::Function) = Plot{func} to_type(Typ::Type{T}) where T <: AbstractPlot = Typ diff --git a/src/ffmpeg-util.jl b/src/ffmpeg-util.jl index 2ae838e6d6d..a3f5eed6fc1 100644 --- a/src/ffmpeg-util.jl +++ b/src/ffmpeg-util.jl @@ -221,7 +221,10 @@ function VideoStream(fig::FigureLike; path = joinpath(dir, "$(gensym(:video)).$(format)") scene = get_scene(fig) update_state_before_display!(fig) - screen = getscreen(backend, scene, GLNative; visible=visible, start_renderloop=false, screen_config...) + config = Dict{Symbol,Any}(screen_config) + get!(config, :visible, visible) + get!(config, :start_renderloop, false) + screen = getscreen(backend, scene, config, GLNative) _xdim, _ydim = size(screen) xdim = iseven(_xdim) ? _xdim : _xdim + 1 ydim = iseven(_ydim) ? _ydim : _ydim + 1 diff --git a/src/figureplotting.jl b/src/figureplotting.jl index 7fe127f6374..49ca2cd8748 100644 --- a/src/figureplotting.jl +++ b/src/figureplotting.jl @@ -3,6 +3,11 @@ struct AxisPlot plot::AbstractPlot end +struct FigureAxis + figure::Figure + axis::Any +end + Base.show(io::IO, fap::FigureAxisPlot) = show(io, fap.figure) Base.show(io::IO, ::MIME"text/plain", fap::FigureAxisPlot) = print(io, "FigureAxisPlot()") @@ -27,94 +32,151 @@ function _disallow_keyword(kw, attributes) end end -@nospecialize -function get_axis(fig, P, axis_kw::Dict, plot_attr, plot_args) - if haskey(axis_kw, :type) - axtype = axis_kw[:type] - pop!(axis_kw, :type) - ax = axtype(fig; axis_kw...) - else - proxyscene = Scene() - # We dont forward the attributes to the plot, since we only need the arguments to determine the axis type - # Remove arguments may not work with plotting into the scene - delete!(plot_attr, :show_axis) - delete!(plot_attr, :limits) - if get(plot_attr, :color, nothing) isa Cycled - # Color may contain Cycled(1), which needs the axis to get resolved to a color - delete!(plot_attr, :color) - end - plot!(proxyscene, P, Attributes(plot_attr), plot_args...) - if is2d(proxyscene) - ax = Axis(fig; axis_kw...) - else - ax = LScene(fig; axis_kw...) - end - empty!(proxyscene) +# For plots that dont require an axis, +# E.g. BlockSpec +struct FigureOnly end + + +function args_preferred_axis(::Type{<:Union{Wireframe,Surface,Contour3d}}, x::AbstractArray, y::AbstractArray, + z::AbstractArray) + return all(x -> z[1] ≈ x, z) ? Axis : LScene +end + +args_preferred_axis(x) = nothing + +function args_preferred_axis(@nospecialize(args...)) + # Fallback: check each single arg if they have a favorite axis type + for arg in args + r = args_preferred_axis(arg) + isnothing(r) || return r end - return ax + return nothing +end + +args_preferred_axis(::AbstractVector, ::AbstractVector, ::AbstractVector, ::Function) = LScene +args_preferred_axis(::AbstractArray{T,3}) where {T} = LScene + +function args_preferred_axis(::AbstractVector{<:Union{AbstractGeometry{DIM},GeometryBasics.Mesh{DIM}}}) where {DIM} + return DIM === 2 ? Axis : LScene +end + +function args_preferred_axis(::Union{AbstractGeometry{DIM},GeometryBasics.Mesh{DIM}}) where {DIM} + return DIM === 2 ? Axis : LScene end -@specialize -function plot(P::PlotFunc, args...; axis=NamedTuple(), figure=NamedTuple(), kw_attributes...) - _validate_nt_like_keyword(axis, "axis") - _validate_nt_like_keyword(figure, "figure") +args_preferred_axis(::AbstractVector{<:Point3}) = LScene +args_preferred_axis(::AbstractVector{<:Point2}) = Axis + + +preferred_axis_type(::Volume) = LScene +preferred_axis_type(::Union{Image,Heatmap}) = Axis - fig = Figure(; figure...) +function preferred_axis_type(p::Plot{F}) where F + # Otherwise, we check the arguments + input_args = map(to_value, p.args) + result = args_preferred_axis(Plot{F}, input_args...) + isnothing(result) || return result + conv_args = map(to_value, p.converted) + result = args_preferred_axis(Plot{F}, conv_args...) + isnothing(result) && return Axis # Fallback to Axis if nothing found + return result +end - axis = Dict(pairs(axis)) - ax = get_axis(fig, P, axis, Dict{Symbol, Any}(kw_attributes), args) +to_dict(dict::Dict) = dict +to_dict(nt::NamedTuple) = Dict{Symbol,Any}(pairs(nt)) +to_dict(attr::Attributes) = attributes(attr) - fig[1, 1] = ax - p = plot!(ax, P, Attributes(kw_attributes), args...) - return FigureAxisPlot(fig, ax, p) +function extract_attributes(dict, key) + attributes = pop!(dict, key, Dict{Symbol,Any}()) + _validate_nt_like_keyword(attributes, key) + return to_dict(attributes) end -# without scenelike, use current axis of current figure +function create_axis_for_plot(figure::Figure, plot::AbstractPlot, attributes::Dict) + axis_kw = extract_attributes(attributes, :axis) + AxType = if haskey(axis_kw, :type) + pop!(axis_kw, :type) + else + preferred_axis_type(plot) + end + if AxType == FigureOnly # For FigureSpec, which creates Axes dynamically + return nothing + end + bbox = pop!(axis_kw, :bbox, nothing) + return _block(AxType, figure, [], axis_kw, bbox) +end -function plot!(P::PlotFunc, args...; kw_attributes...) +function create_axis_like(plot::AbstractPlot, attributes::Dict, ::Nothing) + figure_kw = extract_attributes(attributes, :figure) + figure = Figure(; figure_kw...) + ax = create_axis_for_plot(figure, plot, attributes) + if isnothing(ax) # For FigureSpec + return figure + else + figure[1, 1] = ax + return FigureAxis(figure, ax) + end +end + +MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, s::Union{Plot, Scene}) = s + +function MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, ::Nothing) figure = current_figure() isnothing(figure) && error("There is no current figure to plot into.") + _disallow_keyword(:figure, attributes) ax = current_axis(figure) isnothing(ax) && error("There is no current axis to plot into.") - return plot!(P, ax, args...; kw_attributes...) + _disallow_keyword(:axis, attributes) + return ax end -function plot(P::PlotFunc, gp::GridPosition, args...; axis=NamedTuple(), kw_attributes...) - _validate_nt_like_keyword(axis, "axis") +function MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, gp::GridPosition) + _disallow_keyword(:figure, attributes) + c = contents(gp; exact=true) + if !(length(c) == 1 && can_be_current_axis(c[1])) + error("There needs to be a single axis-like object at $(gp.span), $(gp.side) to plot into.\nUse a non-mutating plotting command to create an axis implicitly.") + end + ax = first(c) + _disallow_keyword(:axis, attributes) + return ax +end + +function create_axis_like(plot::AbstractPlot, attributes::Dict, gp::GridPosition) + _disallow_keyword(:figure, attributes) + figure = get_top_parent(gp) c = contents(gp; exact=true) if !isempty(c) error(""" You have used the non-mutating plotting syntax with a GridPosition, which requires an empty GridLayout slot to create an axis in, but there are already the following objects at this layout position: - $(c) - If you meant to plot into an axis at this position, use the plotting function with `!` (e.g. `func!` instead of `func`). If you really want to place an axis on top of other blocks, make your intention clear and create it manually. """) end - - axis = Dict(pairs(axis)) - fig = get_top_parent(gp) - ax = get_axis(fig, P, axis, Dict{Symbol,Any}(kw_attributes), args) - - gp[] = ax - p = plot!(P, ax, args...; kw_attributes...) - return AxisPlot(ax, p) + ax = create_axis_for_plot(figure, plot, attributes) + if isnothing(ax) # For FigureSpec + return gp + else + gp[] = ax + return ax + end end -function plot!(P::PlotFunc, gp::GridPosition, args...; kwargs...) +function MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, gsp::GridSubposition) + _disallow_keyword(:figure, attributes) + layout = GridLayoutBase.get_layout_at!(gsp.parent; createmissing=false) + gp = layout[gsp.rows, gsp.cols, gsp.side] c = contents(gp; exact=true) if !(length(c) == 1 && can_be_current_axis(c[1])) - error("There needs to be a single axis-like object at $(gp.span), $(gp.side) to plot into.\nUse a non-mutating plotting command to create an axis implicitly.") + error("There is not just one axis at $(gp).") end - ax = first(c) - return plot!(P, ax, args...; kwargs...) + _disallow_keyword(:axis, attributes) + return first(c) end -function plot(P::PlotFunc, gsp::GridSubposition, args...; axis=NamedTuple(), kw_attributes...) - _validate_nt_like_keyword(axis, "axis") - +function create_axis_like(plot::AbstractPlot, attributes::Dict, gsp::GridSubposition) + _disallow_keyword(:figure, attributes) GridLayoutBase.get_layout_at!(gsp.parent; createmissing=true) c = contents(gsp; exact=true) if !isempty(c) @@ -128,28 +190,26 @@ function plot(P::PlotFunc, gsp::GridSubposition, args...; axis=NamedTuple(), kw_ """) end - fig = get_top_parent(gsp) - axis = Dict(pairs(axis)) - ax = get_axis(fig, P, axis, Dict{Symbol,Any}(kw_attributes), args) - + figure = get_top_parent(gsp) + ax = create_axis_for_plot(figure, plot, attributes) gsp.parent[gsp.rows, gsp.cols, gsp.side] = ax - p = plot!(P, ax, args...; kw_attributes...) - return AxisPlot(ax, p) + return ax end -function plot!(P::PlotFunc, gsp::GridSubposition, args...; kwargs...) - layout = GridLayoutBase.get_layout_at!(gsp.parent; createmissing=false) - - gp = layout[gsp.rows, gsp.cols, gsp.side] +function create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, ax::AbstractAxis) + _disallow_keyword(:axis, attributes) + return ax +end - c = contents(gp; exact=true) - if !(length(c) == 1 && can_be_current_axis(c[1])) - error("There is not just one axis at $(gp).") - end - ax = first(c) - return plot!(P, ax, args...; kwargs...) +function create_axis_like(@nospecialize(::AbstractPlot), ::Dict, ::Union{Scene,AbstractAxis}) + return error("Plotting into an axis without !") end +figurelike_return(fa::FigureAxis, plot::AbstractPlot) = FigureAxisPlot(fa.figure, fa.axis, plot) +figurelike_return(ax::AbstractAxis, plot::AbstractPlot) = AxisPlot(ax, plot) +figurelike_return!(::AbstractAxis, plot::AbstractPlot) = plot +figurelike_return!(::Union{Plot, Scene}, plot::AbstractPlot) = plot + update_state_before_display!(f::FigureAxisPlot) = update_state_before_display!(f.figure) function update_state_before_display!(f::Figure) @@ -158,3 +218,87 @@ function update_state_before_display!(f::Figure) end return end + + + +@inline plot_args(args...) = (nothing, args) +@inline function plot_args(a::Union{Figure,AbstractAxis,Scene,Plot,GridSubposition,GridPosition}, + args...) + return (a, args) +end +function fig_keywords!(kws) + figkws = Dict{Symbol,Any}() + if haskey(kws, :axis) + figkws[:axis] = pop!(kws, :axis) + end + if haskey(kws, :figure) + figkws[:figure] = pop!(kws, :figure) + end + return figkws +end + +# Don't inline these, since they will get called from `scatter!(args...; kw...)` which gets specialized to all kw args +@noinline function MakieCore._create_plot(F, attributes::Dict, args...) + figarg, pargs = plot_args(args...) + figkws = fig_keywords!(attributes) + plot = Plot{F}(pargs, attributes) + ax = create_axis_like(plot, figkws, figarg) + plot!(ax, plot) + return figurelike_return(ax, plot) +end + +@noinline function MakieCore._create_plot!(F, attributes::Dict, args...) + figarg, pargs = plot_args(args...) + figkws = fig_keywords!(attributes) + plot = Plot{F}(pargs, attributes) + ax = create_axis_like!(plot, figkws, figarg) + plot!(ax, plot) + return figurelike_return!(ax, plot) +end + +@noinline function MakieCore._create_plot!(F, attributes::Dict, scene::SceneLike, args...) + plot = Plot{F}(args, attributes) + plot!(scene, plot) + return plot +end + +# This enables convert_arguments(::Type{<:AbstractPlot}, ::X) -> FigureSpec +# Which skips axis creation +# TODO, what to return for the dynamically created axes? +figurelike_return(f::GridPosition, p::AbstractPlot) = p +figurelike_return(f::Figure, p::AbstractPlot) = FigureAxisPlot(f, nothing, p) +MakieCore.create_axis_like!(::AbstractPlot, attributes::Dict, fig::Figure) = fig + +# Axis interface + +Makie.can_be_current_axis(ax::AbstractAxis) = true + +function update_state_before_display!(ax::AbstractAxis) + reset_limits!(ax) + return +end + +plot!(fa::FigureAxis, plot) = plot!(fa.axis, plot) + +function plot!(ax::AbstractAxis, plot::AbstractPlot) + plot!(ax.scene, plot) + # some area-like plots basically always look better if they cover the whole plot area. + # adjust the limit margins in those cases automatically. + needs_tight_limits(plot) && tightlimits!(ax) + if is_open_or_any_parent(ax.scene) + reset_limits!(ax) + end + return plot +end + +function Base.delete!(ax::AbstractAxis, plot::AbstractPlot) + delete!(ax.scene, plot) + return ax +end + +function Base.empty!(ax::AbstractAxis) + while !isempty(ax.scene.plots) + delete!(ax, ax.scene.plots[end]) + end + return ax +end diff --git a/src/figures.jl b/src/figures.jl index 6e54b87c4a3..b349ca71635 100644 --- a/src/figures.jl +++ b/src/figures.jl @@ -26,11 +26,12 @@ if an axis is placed at that position (if not it errors) or it can reference an get_scene(fig::Figure) = fig.scene get_scene(fap::FigureAxisPlot) = fap.figure.scene +get_scene(gp::GridLayoutBase.GridPosition) = get_scene(get_figure(gp)) +get_scene(gp::GridLayoutBase.GridSubposition) = get_scene(get_figure(gp)) -const CURRENT_FIGURE = Ref{Union{Nothing, Figure}}(nothing) -Base.@deprecate_binding _current_figure CURRENT_FIGURE +const CURRENT_FIGURE = Ref{Union{Nothing, Figure}}(nothing) const CURRENT_FIGURE_LOCK = Base.ReentrantLock() """ @@ -196,7 +197,7 @@ end # Layouts are already hooked up to this, so it's very simple. """ resize!(fig::Figure, width, height) -Resizes the given `Figure` to the resolution given by `width` and `height`. +Resizes the given `Figure` to the size given by `width` and `height`. If you want to resize the figure to its current layout content, use `resize_to_layout!(fig)` instead. """ Makie.resize!(figure::Figure, width::Integer, height::Integer) = resize!(figure.scene, width, height) diff --git a/src/interaction/events.jl b/src/interaction/events.jl index 9fedf07127f..fbc08f862a5 100644 --- a/src/interaction/events.jl +++ b/src/interaction/events.jl @@ -13,7 +13,7 @@ entered_window(scene, native_window) = not_implemented_for(native_window) function connect_screen(scene::Scene, screen) - on(screen.window_open) do open + on(scene, screen.window_open) do open events(scene).window_open[] = open end @@ -75,8 +75,6 @@ function onpick end ################################################################################ -abstract type BooleanOperator end - """ And(left, right[, rest...]) @@ -224,10 +222,10 @@ create_sets(s::Set) = [Set{Union{Keyboard.Button, Mouse.Button}}(s)] # ispressed and logic evaluation """ - ispressed(parent, result::Bool) - ispressed(parent, button::Union{Mouse.Button, Keyboard.Button) - ispressed(parent, collection::Union{Set, Vector, Tuple}) - ispressed(parent, op::BooleanOperator) +ispressed(parent, result::Bool[, waspressed = nothing]) +ispressed(parent, button::Union{Mouse.Button, Keyboard.Button[, waspressed = nothing]) + ispressed(parent, collection::Union{Set, Vector, Tuple}[, waspressed = nothing]) + ispressed(parent, op::BooleanOperator[, waspressed = nothing]) This function checks if a button or combination of buttons is pressed. @@ -251,25 +249,31 @@ Furthermore you can also make any button, button collection or boolean expression exclusive by wrapping it in `Exclusively(...)`. With that `ispressed` will only return true if the currently pressed buttons match the request exactly. -See also: [`And`](@ref), [`Or`](@ref), [`Not`](@ref), [`Exclusively`](@ref), +For cases where you want to react to a release event you can optionally add +a key or mousebutton `waspressed` which is then assumed to be pressed regardless +of it's current state. For example, when reacting to a mousebutton event, you can +pass `event.button` so that a key combination including that button still evaluates +as true. + +See also: [`waspressed`](@ref) [`And`](@ref), [`Or`](@ref), [`Not`](@ref), [`Exclusively`](@ref), [`&`](@ref), [`|`](@ref), [`!`](@ref) """ -ispressed(events::Events, mb::Mouse.Button) = mb in events.mousebuttonstate -ispressed(events::Events, key::Keyboard.Button) = key in events.keyboardstate -ispressed(parent, result::Bool) = result +ispressed(events::Events, mb::Mouse.Button, waspressed = nothing) = mb in events.mousebuttonstate || mb == waspressed +ispressed(events::Events, key::Keyboard.Button, waspressed = nothing) = key in events.keyboardstate || key == waspressed +ispressed(parent, result::Bool, waspressed = nothing) = result -ispressed(parent, mb::Mouse.Button) = ispressed(events(parent), mb) -ispressed(parent, key::Keyboard.Button) = ispressed(events(parent), key) -@deprecate ispressed(scene, ::Nothing) ispressed(parent, true) +ispressed(parent, mb::Mouse.Button, waspressed = nothing) = ispressed(events(parent), mb, waspressed) +ispressed(parent, key::Keyboard.Button, waspressed = nothing) = ispressed(events(parent), key, waspressed) # Boolean Operator evaluation -ispressed(parent, op::And) = ispressed(parent, op.left) && ispressed(parent, op.right) -ispressed(parent, op::Or) = ispressed(parent, op.left) || ispressed(parent, op.right) -ispressed(parent, op::Not) = !ispressed(parent, op.x) -ispressed(parent, op::Exclusively) = ispressed(events(parent), op) -ispressed(e::Events, op::Exclusively) = op.x == union(e.keyboardstate, e.mousebuttonstate) +ispressed(parent, op::And, waspressed = nothing) = ispressed(parent, op.left, waspressed) && ispressed(parent, op.right, waspressed) +ispressed(parent, op::Or, waspressed = nothing) = ispressed(parent, op.left, waspressed) || ispressed(parent, op.right, waspressed) +ispressed(parent, op::Not, waspressed = nothing) = !ispressed(parent, op.x, waspressed) +ispressed(parent, op::Exclusively, waspressed = nothing) = ispressed(events(parent), op, waspressed) +ispressed(e::Events, op::Exclusively, waspressed::Union{Mouse.Button, Keyboard.Button}) = op.x == union(e.keyboardstate, e.mousebuttonstate, waspressed) +ispressed(e::Events, op::Exclusively, waspressed = nothing) = op.x == union(e.keyboardstate, e.mousebuttonstate) # collections -ispressed(parent, set::Set) = all(x -> ispressed(parent, x), set) -ispressed(parent, set::Vector) = all(x -> ispressed(parent, x), set) -ispressed(parent, set::Tuple) = all(x -> ispressed(parent, x), set) +ispressed(parent, set::Set, waspressed = nothing) = all(x -> ispressed(parent, x, waspressed), set) +ispressed(parent, set::Vector, waspressed = nothing) = all(x -> ispressed(parent, x, waspressed), set) +ispressed(parent, set::Tuple, waspressed = nothing) = all(x -> ispressed(parent, x, waspressed), set) diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index 0fce5bbe703..228ea7c6e20 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -125,12 +125,12 @@ A --- B | | D --- C -this computes parameter `f` such that the line from `A + f * (B - A)` to -`D + f * (C - D)` crosses through the given point `P`. This assumes that `P` is +this computes parameter `f` such that the line from `A + f * (B - A)` to +`D + f * (C - D)` crosses through the given point `P`. This assumes that `P` is inside the quad and that none of the edges cross. """ function point_in_quad_parameter( - A::Point2, B::Point2, C::Point2, D::Point2, P::Point2; + A::Point2, B::Point2, C::Point2, D::Point2, P::Point2; iterations = 50, epsilon = 1e-6 ) @@ -166,9 +166,9 @@ end function shift_project(scene, pos) project( camera(scene).projectionview[], - Vec2f(widths(pixelarea(scene)[])), + Vec2f(size(scene)), pos - ) .+ Vec2f(origin(pixelarea(scene)[])) + ) .+ Vec2f(origin(viewport(scene)[])) end @@ -236,7 +236,7 @@ returning a label. See Makie documentation for more detail. - `enable_indicators = true)`: Enables or disables indicators - `depth = 9e3`: Depth value of the tooltip. This should be high so that the tooltip is always in front. -- `apply_tooltip_offset = true`: Enables or disables offsetting tooltips based +- `apply_tooltip_offset = true`: Enables or disables offsetting tooltips based on, for example, markersize. - and all attributes from `Tooltip` """ @@ -246,7 +246,7 @@ end function DataInspector(scene::Scene; priority = 100, kwargs...) parent = root(scene) - @assert origin(pixelarea(parent)[]) == Vec2f(0) + @assert origin(viewport(parent)[]) == Vec2f(0) attrib_dict = Dict(kwargs) base_attrib = Attributes( @@ -397,7 +397,7 @@ end function update_tooltip_alignment!(inspector, proj_pos) inspector.plot[1][] = proj_pos - wx, wy = widths(pixelarea(inspector.root)[]) + wx, wy = widths(viewport(inspector.root)[]) px, py = proj_pos placement = py < 0.75wy ? (:above) : (:below) @@ -511,13 +511,12 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i # cast ray from cursor into screen, find closest point to line pos = position_on_plot(plot, idx) - proj_pos = shift_project(scene, pos) update_tooltip_alignment!(inspector, proj_pos) tt.offset[] = ifelse( - a.apply_tooltip_offset[], - sv_getindex(plot.linewidth[], idx) + 2, + a.apply_tooltip_offset[], + sv_getindex(plot.linewidth[], idx) + 2, a.offset[] ) @@ -561,7 +560,7 @@ 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 @@ -673,16 +672,16 @@ function show_imagelike(inspector, plot, name, edge_based) if a.enable_indicators[] if plot.interpolate[] - if inspector.selection != plot || (length(inspector.temp_plots) != 1) || + if inspector.selection != plot || (length(inspector.temp_plots) != 1) || !(inspector.temp_plots[1] isa Scatter) clear_temporary_plots!(inspector, plot) p = scatter!( scene, pos, color = a._color, visible = a.indicator_visible, inspectable = false, model = plot.model, - # TODO switch to Rect with 2r-1 or 2r-2 markersize to have + # 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), + marker=:rect, markersize = map(r -> 2r, a.range), strokecolor = a.indicator_color, strokewidth = a.indicator_linewidth, depth_shift = -1f-3 @@ -695,7 +694,7 @@ function show_imagelike(inspector, plot, name, edge_based) end else bbox = _pixelated_image_bbox(plot[1][], plot[2][], plot[3][], i, j, edge_based) - if inspector.selection != plot || (length(inspector.temp_plots) != 1) || + if inspector.selection != plot || (length(inspector.temp_plots) != 1) || !(inspector.temp_plots[1] isa Wireframe) clear_temporary_plots!(inspector, plot) p = wireframe!( @@ -791,7 +790,7 @@ end ################################################################################ -### show_data for Combined/recipe plots +### show_data for Plot/recipe plots ################################################################################ @@ -919,7 +918,7 @@ function show_poly(inspector, plot, poly, 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, depth_shift = -1f-3 @@ -954,7 +953,7 @@ function show_data(inspector::DataInspector, plot::VolumeSlices, idx, child::Hea 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) @@ -1004,7 +1003,7 @@ function show_data(inspector::DataInspector, plot::Band, idx::Integer, mesh::Mes clear_temporary_plots!(inspector, plot) p = lines!( scene, [P1, P2], transformation = Transformation(plot.transformation), - color = a.indicator_color, strokewidth = a.indicator_linewidth, + color = a.indicator_color, strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a.indicator_visible, inspectable = false, depth_shift = -1f-3 diff --git a/src/interaction/interactive_api.jl b/src/interaction/interactive_api.jl index 7ea6243d497..4fea246ad69 100644 --- a/src/interaction/interactive_api.jl +++ b/src/interaction/interactive_api.jl @@ -31,8 +31,6 @@ function onpick(f, scene::Scene, plots::AbstractPlot...; range=1) end end -@deprecate mouse_selection pick - """ mouse_in_scene(fig/ax/scene[, priority = 0]) @@ -44,7 +42,7 @@ function mouse_in_scene(scene::Scene; priority = 0) p = rootparent(scene) output = Observable(Vec2(0.0)) on(events(scene).mouseposition, priority = priority) do mp - output[] = Vec(mp) .- minimum(pixelarea(scene)[]) + output[] = Vec(mp) .- minimum(viewport(scene)[]) return Consume(false) end output @@ -175,7 +173,7 @@ Normalizes mouse position `pos` relative to the screen rectangle. """ screen_relative(x, mpos) = screen_relative(get_scene(x), mpos) function screen_relative(scene::Scene, mpos) - return Point2f(mpos) .- Point2f(minimum(pixelarea(scene)[])) + return Point2f(mpos) .- Point2f(minimum(viewport(scene)[])) end """ diff --git a/src/interaction/observables.jl b/src/interaction/observables.jl index a1d7b2d4465..6b384efa4d0 100644 --- a/src/interaction/observables.jl +++ b/src/interaction/observables.jl @@ -17,34 +17,11 @@ function safe_off(o::Observables.AbstractObservable, f) end end -""" - map_once(closure, inputs::Observable....)::Observable - -Like Reactive.foreach, in the sense that it will be preserved even if no reference is kept. -The difference is, that you can call map once multiple times with the same closure and it will -close the old result Observable and register a new one instead. - -``` -function test(s1::Observable) - s3 = map_once(x-> (println("1 ", x); x), s1) - s3 = map_once(x-> (println("2 ", x); x), s1) - -end -test(Observable(1), Observable(2)) -> - -""" -function map_once( - f, input::Observable, inputrest::Observable... - ) - for arg in (input, inputrest...) - safe_off(arg, f) - end - lift(f, input, inputrest...) +function on_latest(f, observable::Observable; update=false, spawn=false) + return on_latest(f, nothing, observable; update=update, spawn=spawn) end -function on_latest(f, observable; update=false, spawn=false) - # How does one create a finished task?? +function on_latest(f, to_track, observable::Observable; update=false, spawn=false) last_task = nothing has_changed = Threads.Atomic{Bool}(false) function run_f(new_value) @@ -61,15 +38,12 @@ function on_latest(f, observable; update=false, spawn=false) # we assume for now that `==` is prohibitive as the default if has_changed[] has_changed[] = false - run_f(observable[]) # needs to recursive + run_f(observable[]) # needs to be recursive end end - return on(observable; update=update) do new_value - if isnothing(last_task) - # run first task in sync - last_task = update ? (@async f(observable[])) : @async(nothing) - wait(last_task) - elseif istaskdone(last_task) + + function on_callback(new_value) + if isnothing(last_task) || istaskdone(last_task) if spawn last_task = Threads.@spawn run_f(new_value) else @@ -80,6 +54,14 @@ function on_latest(f, observable; update=false, spawn=false) return # Do nothing if working end end + + update && f(observable[]) + + if isnothing(to_track) + return on(on_callback, observable) + else + return on(on_callback, to_track, observable) + end end function onany_latest(f, observables...; update=false, spawn=false) @@ -87,3 +69,14 @@ function onany_latest(f, observables...; update=false, spawn=false) onany((args...)-> (result[] = args), observables...) on_latest((args)-> f(args...), result; update=update, spawn=spawn) end + +function map_latest!(f, result::Observable, observables...; update=false, spawn=false) + callback = Observables.MapCallback(f, result, observables) + return onany_latest(callback, observables...; update=update, spawn=spawn) +end + +function map_latest(f, observables...; spawn=false, ignore_equal_values=false) + result = Observable(f(map(to_value, observables)...); ignore_equal_values=ignore_equal_values) + map_latest!(f, result, observables...; update=update, spawn=spawn) + return result +end diff --git a/src/interaction/ray_casting.jl b/src/interaction/ray_casting.jl index f87ffe4bae3..8b3c67f2515 100644 --- a/src/interaction/ray_casting.jl +++ b/src/interaction/ray_casting.jl @@ -21,7 +21,7 @@ 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 +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) @@ -35,12 +35,12 @@ function Ray(scene::Scene, cam::Camera3D, xy::VecTypes{2}) 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[]) + + px_width, px_height = widths(scene) aspect = px_width / px_height rel_pos = 2 .* xy ./ (px_width, px_height) .- 1 - if cam.attributes.projectiontype[] === Perspective + if cam.settings.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 @@ -51,7 +51,7 @@ function Ray(scene::Scene, cam::Camera3D, xy::VecTypes{2}) end function Ray(scene::Scene, cam::Camera2D, xy::VecTypes{2}) - rel_pos = xy ./ widths(scene.px_area[]) + rel_pos = xy ./ widths(scene) pv = scene.camera.projectionview[] m = Vec2f(pv[1, 1], pv[2, 2]) b = Vec2f(pv[1, 4], pv[2, 4]) @@ -64,16 +64,16 @@ function Ray(::Scene, ::PixelCamera, xy::VecTypes{2}) end function Ray(scene::Scene, ::RelativeCamera, xy::VecTypes{2}) - origin = xy ./ widths(scene.px_area[]) + origin = xy ./ widths(scene) 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 +# This method should always work function ray_from_projectionview(scene::Scene, xy::VecTypes{2}) inv_view_proj = inv(camera(scene).projectionview[]) - area = pixelarea(scene)[] + area = viewport(scene)[] # This figures out the camera view direction from the projectionview matrix # and computes a ray from a near and a far point. @@ -124,6 +124,12 @@ function closest_point_on_line(A::Point3f, B::Point3f, ray::Ray) return A .+ clamp(t, 0.0, AB_norm) * u_AB end +function ray_triangle_intersection(A::VecTypes, B::VecTypes, C::VecTypes, ray::Ray, ϵ = 1e-6) + return ray_triangle_intersection( + to_ndim(Point3f, A, 0f0), to_ndim(Point3f, B, 0f0), to_ndim(Point3f, C, 0f0), + ray, ϵ + ) +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 @@ -182,7 +188,7 @@ 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))[])) +ray = Ray(parent_scene(plot), xy .- minimum(viewport(parent_scene(plot))[])) position = position_on_plot(plot, idx, ray, apply_transform = true) ``` See [`position_on_plot`](@ref) for more information. @@ -191,7 +197,7 @@ function ray_assisted_pick(obj, xy = events(obj).mouseposition[]; apply_transfor 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)[])) + ray = Ray(scene, xy .- minimum(viewport(scene)[])) pos = position_on_plot(plot, idx, ray, apply_transform = apply_transform) return (plot, idx, pos) end @@ -200,23 +206,23 @@ 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 +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)[]))) +pos_in_ax = position_on_plot(plot, idx, Ray(ax, px_pos .- minimum(viewport(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 +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 +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)` @@ -227,26 +233,28 @@ differs: """ function position_on_plot(plot::AbstractPlot, idx::Integer; apply_transform = true) return position_on_plot( - plot, idx, ray_at_cursor(parent_scene(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) + point = plot[1][][idx] + point3f = to_ndim(Point3f, point, 0.0f0) + point_t = if apply_transform && !isnan(point3f) + apply_transform_and_model(plot, point3f) else - return pos + point3f end + return to_ndim(typeof(point), point_t, 0.0f0) 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 @@ -258,8 +266,8 @@ function position_on_plot(plot::Union{Lines, LineSegments}, idx, ray::Ray; apply 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 + # 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)) @@ -268,7 +276,7 @@ function position_on_plot(plot::Union{Heatmap, Image}, idx, ray::Ray; apply_tran 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] @@ -300,7 +308,7 @@ function position_on_plot(plot::Mesh, idx, ray::Ray; apply_transform = true) end end - @info "Did not find $idx" + @debug "Did not find intersection for index = $idx when casting a ray on mesh." return Point3f(NaN) end @@ -329,7 +337,7 @@ function position_on_plot(plot::Surface, idx, ray::Ray; apply_transform = true) 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 @@ -387,4 +395,4 @@ function position_on_plot(plot::Volume, idx, ray::Ray; apply_transform = true) 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 +position_on_plot(plot::Nothing, args...; kwargs...) = Point3f(NaN) diff --git a/src/interfaces.jl b/src/interfaces.jl index c040da6cdd3..41d5e19bc56 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -1,9 +1,28 @@ + +function add_cycle_attribute!(plot::Plot, scene::Scene, cycle=get_cycle_for_plottype(plot.cycle[])) + cycler = scene.cycler + palette = scene.theme.palette + add_cycle_attributes!(plot, cycle, cycler, palette) + return +end + function color_and_colormap!(plot, colors = plot.color) + scene = parent_scene(plot) + if !isnothing(scene) && haskey(plot, :cycle) + add_cycle_attribute!(plot, scene) + end colors = assemble_colors(colors[], colors, plot) attributes(plot.attributes)[:calculated_colors] = colors end -function calculated_attributes!(T::Type{<: Mesh}, plot) +function calculated_attributes!(::Type{<: AbstractPlot}, plot) + scene = parent_scene(plot) + if !isnothing(scene) && haskey(plot, :cycle) + add_cycle_attribute!(plot, scene) + end +end + +function calculated_attributes!(::Type{<: Mesh}, plot) mesha = lift(GeometryBasics.attributes, plot, plot.mesh) color = haskey(mesha[], :color) ? lift(x-> x[:color], plot, mesha) : plot.color color_and_colormap!(plot, color) @@ -69,7 +88,8 @@ function calculated_attributes!(::Type{T}, plot) where {T<:Union{Lines, LineSegm for attr in [:color, :linewidth] # taken from @edljk in PR #77 if haskey(plot, attr) && isa(plot[attr][], AbstractVector) && (length(pos) ÷ 2) == length(plot[attr][]) - plot[attr] = lift(plot, plot[attr]) do cols + # TODO, this is actually buggy for `plot.color = new_colors`, since we're overwriting the origin color input + attributes(plot.attributes)[attr] = lift(plot, plot[attr]) do cols map(i -> cols[(i + 1) ÷ 2], 1:(length(cols) * 2)) end end @@ -79,16 +99,56 @@ function calculated_attributes!(::Type{T}, plot) where {T<:Union{Lines, LineSegm return end -const atomic_function_symbols = ( - :text, :meshscatter, :scatter, :mesh, :linesegments, - :lines, :surface, :volume, :heatmap, :image +const atomic_functions = ( + text, meshscatter, scatter, mesh, linesegments, + lines, surface, volume, heatmap, image ) +const Atomic{Arg} = Union{map(x-> Plot{x, Arg}, atomic_functions)...} + +function convert_arguments!(plot::Plot{F}) where {F} + P = Plot{F,Any} + function on_update(kw, args...) + nt = convert_arguments(P, args...; kw...) + pnew, converted = apply_convert!(P, plot.attributes, nt) + @assert plotfunc(pnew) === F "Changed the plot type in convert_arguments. This isn't allowed!" + for (obs, new_val) in zip(plot.converted, converted) + obs[] = new_val + end + end + used_attrs = used_attributes(P, to_value.(plot.args)...) + convert_keys = intersect(used_attrs, keys(plot.attributes)) + kw_signal = if isempty(convert_keys) + # lift(f) isn't supported so we need to catch the empty case + Observable(()) + else + # Remove used attributes from `attributes` and collect them in a `Tuple` to pass them more easily + lift((args...) -> Pair.(convert_keys, args), plot, pop!.(plot.attributes, convert_keys)...) + end + onany(on_update, plot, kw_signal, plot.args...) + return +end + +function Plot{Func}(args::Tuple, plot_attributes::Dict) where {Func} + if !isempty(args) && first(args) isa Attributes + merge!(plot_attributes, attributes(first(args))) + return Plot{Func}(Base.tail(args), plot_attributes) + end + P = Plot{Func} + used_attrs = used_attributes(P, to_value.(args)...) + if used_attrs === () + args_converted = convert_arguments(P, map(to_value, args)...) + else + kw = [Pair(k, to_value(v)) for (k, v) in plot_attributes if k in used_attrs] + args_converted = convert_arguments(P, map(to_value, args)...; kw...) + end + PNew, converted = apply_convert!(P, Attributes(), args_converted) -const atomic_functions = getfield.(Ref(Makie), atomic_function_symbols) -const Atomic{Arg} = Union{map(x-> Combined{x, Arg}, atomic_functions)...} + obs_args = Any[convert(Observable, x) for x in args] -function (PT::Type{<: Combined})(parent, transformation, attributes, input_args, converted) - PT(parent, transformation, attributes, input_args, converted, AbstractPlot[]) + ArgTyp = MakieCore.argtypes(converted) + converted_obs = map(Observable, converted) + plot = Plot{plotfunc(PNew),ArgTyp}(plot_attributes, obs_args, converted_obs) + return plot end """ @@ -113,107 +173,16 @@ Usage: end ``` """ -used_attributes(PlotType, args...) = () +used_attributes(::Type{<:Plot}, args...) = used_attributes(args...) +used_attributes(args...) = () -""" -apply for return type - (args...,) -""" -function apply_convert!(P, attributes::Attributes, x::Tuple) - return (plottype(P, x...), x) -end - -""" -apply for return type PlotSpec -""" -function apply_convert!(P, attributes::Attributes, x::PlotSpec{S}) where S - args, kwargs = x.args, x.kwargs - # Note that kw_args in the plot spec that are not part of the target plot type - # will end in the "global plot" kw_args (rest) - for (k, v) in pairs(kwargs) - attributes[k] = v - end - return (plottype(S, P), args) -end - -function seperate_tuple(args::Observable{<: NTuple{N, Any}}) where N - ntuple(N) do i - lift(args) do x - if i <= length(x) - x[i] - else - error("You changed the number of arguments. This isn't allowed!") - end - end - end -end - -function (PlotType::Type{<: AbstractPlot{Typ}})(scene::SceneLike, attributes::Attributes, args) where Typ - input = convert.(Observable, args) - aconvert(args...) = convert_arguments(PlotType, args...) - argnodes = lift(aconvert, input...) - plot = PlotType(scene, attributes, input, argnodes) - # Manually register obsfuncs, since we can't do lift(aconvert, plot, input...) - for arg in input - push!(plot.deregister_callbacks, Observables.ObserverFunction(aconvert, arg, false)) - end - return plot -end - -function plot(scene::Scene, plot::AbstractPlot) - # plot object contains local theme (default values), and user given values (from constructor) - # fill_theme now goes through all values that are missing from the user, and looks if the scene - # contains any theming values for them (via e.g. css rules). If nothing founds, the values will - # be taken from local theme! This will connect any values in the scene's theme - # with the plot values and track those connection, so that we can separate them - # when doing delete!(scene, plot)! - complete_theme!(scene, plot) - # we just return the plot... whoever calls plot (our pipeline usually) - # will need to push!(scene, plot) etc! - return plot -end - -function (PlotType::Type{<: AbstractPlot{Typ}})(scene::SceneLike, attributes::Attributes, input, args) where Typ - # The argument type of the final plot object is the assumened to stay constant after - # argument conversion. This might not always hold, but it simplifies - # things quite a bit - ArgTyp = typeof(to_value(args)) - # construct the fully qualified plot type, from the possible incomplete (abstract) - # PlotType - - FinalType = Combined{Typ, ArgTyp} - plot_attributes = merged_get!( - ()-> default_theme(scene, FinalType), - plotsym(FinalType), scene, attributes - ) - - # Transformation is a field of the plot type, but can be given as an attribute - trans = get(plot_attributes, :transformation, automatic) - transval = to_value(trans) - transformation = if transval === automatic - Transformation(scene) - elseif isa(transval, Transformation) - transval - else - t = Transformation(scene) - transform!(t, transval) - t - end - replace_automatic!(plot_attributes, :model) do - transformation.model - end - # create the plot, with the full attributes, the input signals, and the final signals. - plot_obj = FinalType(scene, transformation, plot_attributes, input, seperate_tuple(args)) - calculated_attributes!(plot_obj) - plot_obj -end ## generic definitions -# If the Combined has no plot func, calculate them -plottype(::Type{<: Combined{Any}}, argvalues...) = plottype(argvalues...) +# If the Plot has no plot func, calculate them +plottype(::Type{<: Plot{Any}}, argvalues...) = plottype(argvalues...) plottype(::Type{Any}, argvalues...) = plottype(argvalues...) # If it has something more concrete than Any, use it directly -plottype(P::Type{<: Combined{T}}, argvalues...) where T = P +plottype(P::Type{<: Plot{T}}, argvalues...) where T = P ## specialized definitions for types plottype(::AbstractVector, ::AbstractVector, ::AbstractVector) = Scatter @@ -233,7 +202,7 @@ plottype(::AbstractVector{<:GeometryBasics.AbstractPolygon}) = Poly plottype(::MultiPolygon) = Lines """ - plottype(P1::Type{<: Combined{T1}}, P2::Type{<: Combined{T2}}) + plottype(P1::Type{<: Plot{T1}}, P2::Type{<: Plot{T2}}) Chooses the more concrete plot type ```julia @@ -243,169 +212,66 @@ function convert_arguments(P::PlotFunc, args...) end ``` """ -plottype(P1::Type{<: Combined{Any}}, P2::Type{<: Combined{T}}) where T = P2 -plottype(P1::Type{<: Combined{T}}, P2::Type{<: Combined}) where T = P1 +plottype(::Type{<: Plot{Any}}, P::Type{<: Plot{T}}) where T = P +plottype(P::Type{<: Plot{T}}, ::Type{<: Plot}) where T = P # all the plotting functions that get a plot type -const PlotFunc = Union{Type{Any}, Type{<: AbstractPlot}} - - -###################################################################### -# In this section, the plotting functions have P as the first argument -# These are called from type recipes - -function plot!(P::PlotFunc, scene::SceneLike, args...; kw_attributes...) - attributes = Attributes(kw_attributes) - plot!(scene, P, attributes, args...) -end +const PlotFunc = Union{Type{Any},Type{<:AbstractPlot}} -# with positional attributes - -function plot!(P::PlotFunc, scene::SceneLike, attrs::Attributes, args...; kw_attributes...) - attributes = merge!(Attributes(kw_attributes), attrs) - plot!(scene, P, attributes, args...) +function plot!(::Plot{F}) where {F} + if !(F in atomic_functions) + error("No recipe for $(F)") + end end -###################################################################### -# plots to scene +function connect_plot!(parent::SceneLike, plot::Plot{F}) where {F} + plot.parent = parent -""" -Main plotting signatures that plot/plot! route to if no Plot Type is given -""" -function plot!(scene::Union{Combined, SceneLike}, P::PlotFunc, attributes::Attributes, args...; kw_attributes...) - attributes = merge!(Attributes(kw_attributes), attributes) - argvalues = to_value.(args) - pre_type_no_args = plottype(P, argvalues...) - # plottype will lose the argument types, so we just extract the plot func - # type and recreate the type with the argument type - PreType = Combined{plotfunc(pre_type_no_args), typeof(argvalues)} - used_attrs = used_attributes(PreType, argvalues...) - convert_keys = intersect(used_attrs, keys(attributes)) - kw_signal = if isempty(convert_keys) - # lift(f) isn't supported so we need to catch the empty case - Observable(()) + apply_theme!(parent_scene(parent), plot) + t_user = to_value(get(attributes(plot), :transformation, automatic)) + if t_user isa Transformation + plot.transformation = t_user else - # Remove used attributes from `attributes` and collect them in a `Tuple` to pass them more easily - lift((args...) -> Pair.(convert_keys, args), scene, pop!.(attributes, convert_keys)...) - end - # call convert_arguments for a first time to get things started - converted = convert_arguments(PreType, argvalues...; kw_signal[]...) - # convert_arguments can return different things depending on the recipe type - # apply_conversion deals with that! - - FinalType, argsconverted = apply_convert!(PreType, attributes, converted) - converted_node = Observable(argsconverted) - input_nodes = convert.(Observable, args) - obs_funcs = onany(kw_signal, input_nodes...) do kwargs, args... - # do the argument conversion inside a lift - result = convert_arguments(FinalType, args...; kwargs...) - finaltype, argsconverted_ = apply_convert!(FinalType, attributes, result) # avoid a Core.Box (https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured) - if finaltype != FinalType - error("Plot type changed from $FinalType to $finaltype after conversion. - Changing the plot type based on values in convert_arguments is not allowed" - ) + if t_user isa Automatic + plot.transformation = Transformation() + else + t = Transformation() + transform!(t, t_user) + plot.transformation = t end - converted_node[] = argsconverted_ - end - plot_object = plot!(scene, FinalType, attributes, input_nodes, converted_node) - # bind observable clean up to plot object: - append!(plot_object.deregister_callbacks, obs_funcs) - return plot_object -end - -plot!(p::Combined) = _plot!(p) - -_plot!(p::Atomic{T}) where T = p - -function _plot!(p::Combined{fn, T}) where {fn, T} - throw(PlotMethodError(fn, T)) -end - -struct PlotMethodError <: Exception - fn - T -end - -function Base.showerror(io::IO, err::PlotMethodError) - fn = err.fn - T = err.T - args = (T.parameters...,) - typed_args = join(string.("::", args), ", ") - - print(io, "PlotMethodError: no ") - printstyled(io, fn == Any ? "plot" : fn; color=:cyan) - print(io, " method for arguments ") - printstyled(io, "($typed_args)"; color=:cyan) - print(io, ". To support these arguments, define\n ") - printstyled(io, "plot!(::$(Combined{fn,S} where {S<:T}))"; color=:cyan) - print(io, "\nAvailable methods are:\n") - for m in methods(plot!) - if m.sig <: Tuple{typeof(plot!), Combined{fn}} - println(io, " ", m) + if is_space_compatible(plot, parent) + obsfunc = connect!(transformation(parent), transformation(plot)) + append!(plot.deregister_callbacks, obsfunc) end end + plot.model = transformationmatrix(plot) + convert_arguments!(plot) + calculated_attributes!(Plot{F}, plot) + default_shading!(plot, parent_scene(parent)) + plot!(plot) + return plot end -function show_attributes(attributes) - for (k, v) in attributes - println(" ", k, ": ", v[] == nothing ? "nothing" : v[]) - end +function plot!(scene::SceneLike, plot::Plot) + connect_plot!(scene, plot) + push!(scene, plot) + return plot end -""" - extract_scene_attributes!(attributes) - -removes all scene attributes from `attributes` and returns them in a new -Attribute dict. -""" -function extract_scene_attributes!(attributes) - scene_attributes = ( - :backgroundcolor, - :resolution, - :show_axis, - :show_legend, - :scale_plot, - :center, - :axis, - :axis2d, - :axis3d, - :legend, - :camera, - :limits, - :padding, - :raw, - :SSAO - ) - result = Attributes() - for k in scene_attributes - haskey(attributes, k) && (result[k] = pop!(attributes, k)) +function apply_theme!(scene::Scene, plot::P) where {P<: Plot} + raw_attr = attributes(plot.attributes) + plot_theme = default_theme(scene, P) + plot_sym = plotsym(P) + if haskey(theme(scene), plot_sym) + merge_without_obs_reverse!(plot_theme, theme(scene, plot_sym)) end - return result -end -function plot!(scene::SceneLike, P::PlotFunc, attributes::Attributes, input::NTuple{N, Observable}, args::Observable) where {N} - # create "empty" plot type - empty meaning containing no plots, just attributes + arguments - scene_attributes = extract_scene_attributes!(attributes) - if haskey(attributes, :textsize) - throw(ArgumentError("The attribute `textsize` has been renamed to `fontsize` in Makie v0.19. Please change all occurrences of `textsize` to `fontsize` or revert back to an earlier version.")) - end - plot_object = P(scene, attributes, input, args) - # transfer the merged attributes from theme and user defined to the scene - for (k, v) in scene_attributes - error("setting $k for scene via plot attribute not supported anymore") + for (k, v) in plot.kw + if v isa NamedTuple + raw_attr[k] = Attributes(v) + else + raw_attr[k] = convert(Observable{Any}, v) + end end - # call user defined recipe overload to fill the plot type - plot!(plot_object) - push!(scene, plot_object) - return plot_object -end - -function plot!(scene::Combined, P::PlotFunc, attributes::Attributes, input::NTuple{N,Observable}, args::Observable) where {N} - # create "empty" plot type - empty meaning containing no plots, just attributes + arguments - - plot_object = P(scene, attributes, input, args) - # call user defined recipe overload to fill the plot type - plot!(plot_object) - push!(scene.plots, plot_object) - plot_object + return merge!(plot.attributes, plot_theme) end diff --git a/src/layouting/data_limits.jl b/src/layouting/data_limits.jl index 676368c76fb..92d4e94a38b 100644 --- a/src/layouting/data_limits.jl +++ b/src/layouting/data_limits.jl @@ -106,7 +106,7 @@ end foreach_plot(f, s::Figure) = foreach_plot(f, s.scene) foreach_plot(f, s::FigureAxisPlot) = foreach_plot(f, s.figure) foreach_plot(f, list::AbstractVector) = foreach(f, list) -function foreach_plot(f, plot::Combined) +function foreach_plot(f, plot::Plot) if isempty(plot.plots) f(plot) else @@ -166,13 +166,14 @@ function update_boundingbox!(bb_ref, bb::Rect) return end +# Default data_limits function data_limits(plot::AbstractPlot) # Assume primitive plot if isempty(plot.plots) return limits_from_transformed_points(iterate_transformed(plot)) end - # Assume Combined Plot + # Assume Plot Plot bb_ref = Base.RefValue(data_limits(plot.plots[1])) for i in 2:length(plot.plots) update_boundingbox!(bb_ref, data_limits(plot.plots[i])) @@ -181,7 +182,7 @@ function data_limits(plot::AbstractPlot) return bb_ref[] end -function _update_rect(rect::Rect{N, T}, point::Point{N, T}) where {N, T} +function _update_rect(rect::Rect{N, T}, point::VecTypes{N, T}) where {N, T} mi = minimum(rect) ma = maximum(rect) mis_mas = map(mi, ma, point) do _mi, _ma, _p @@ -201,6 +202,22 @@ function limits_from_transformed_points(points_iterator) return bb end +# include bbox from scaled markers +function limits_from_transformed_points(positions, scales, rotations, element_bbox) + isempty(positions) && return Rect3f() + + first_scale = attr_broadcast_getindex(scales, 1) + first_rot = attr_broadcast_getindex(rotations, 1) + full_bbox = Ref(first_rot * (element_bbox * first_scale) + first(positions)) + for (i, pos) in enumerate(positions) + scale, rot = attr_broadcast_getindex(scales, i), attr_broadcast_getindex(rotations, i) + transformed_bbox = rot * (element_bbox * scale) + pos + update_boundingbox!(full_bbox, transformed_bbox) + end + + return full_bbox[] +end + function data_limits(scenelike, exclude=(p)-> false) bb_ref = Base.RefValue(Rect3f()) foreach_plot(scenelike) do plot @@ -232,3 +249,20 @@ function data_limits(plot::Image) maxi = Vec3f(last.(mini_maxi)..., 0) return Rect3f(mini, maxi .- mini) end + +function data_limits(plot::MeshScatter) + # TODO: avoid mesh generation here if possible + @get_attribute plot (marker, markersize, rotations) + marker_bb = Rect3f(marker) + positions = iterate_transformed(plot) + scales = markersize + # fast path for constant markersize + if scales isa VecTypes{3} && rotations isa Quaternion + bb = limits_from_transformed_points(positions) + marker_bb = rotations * (marker_bb * scales) + return Rect3f(minimum(bb) + minimum(marker_bb), widths(bb) + widths(marker_bb)) + else + # TODO: optimize const scale, var rot and var scale, const rot + return limits_from_transformed_points(positions, scales, rotations, marker_bb) + end +end diff --git a/src/layouting/transformation.jl b/src/layouting/transformation.jl index 18a640704cc..840740f8a80 100644 --- a/src/layouting/transformation.jl +++ b/src/layouting/transformation.jl @@ -1,49 +1,20 @@ Base.parent(t::Transformation) = isassigned(t.parent) ? t.parent[] : nothing -function Transformation(transform_func=identity; - scale=Vec3f(1), - translation=Vec3f(0), - rotation=Quaternionf(0, 0, 0, 1)) - - scale_o = convert(Observable{Vec3f}, scale) - translation_o = convert(Observable{Vec3f}, translation) - rotation_o = convert(Observable{Quaternionf}, rotation) - model = map(transformationmatrix, translation_o, scale_o, rotation_o) - return Transformation( - translation_o, - scale_o, - rotation_o, - model, - convert(Observable{Any}, transform_func) - ) -end - -function Transformation(transformable::Transformable; - scale=Vec3f(1), - translation=Vec3f(0), - rotation=Quaternionf(0, 0, 0, 1), - transform_func = copy(transformation(transformable).transform_func)) - - scale_o = convert(Observable{Vec3f}, scale) - translation_o = convert(Observable{Vec3f}, translation) - rotation_o = convert(Observable{Quaternionf}, rotation) - parent_transform = transformation(transformable) - - pmodel = parent_transform.model - model = map(translation_o, scale_o, rotation_o, pmodel) do t, s, r, p - return p * transformationmatrix(t, s, r) +function Observables.connect!(parent::Transformation, child::Transformation; connect_func=true) + tfuncs = [] + obsfunc = on(parent.model; update=true) do m + return child.parent_model[] = m end - - trans = Transformation( - translation_o, - scale_o, - rotation_o, - model, - convert(Observable{Any}, transform_func) - ) - - trans.parent[] = parent_transform - return trans + push!(tfuncs, obsfunc) + if connect_func + t2 = on(parent.transform_func; update=true) do f + child.transform_func[] = f + return + end + push!(tfuncs, t2) + end + child.parent[] = parent + return tfuncs end function free(transformation::Transformation) @@ -69,7 +40,7 @@ end function translated(scene::Scene; kw_args...) tscene = Scene(scene, transformation = Transformation()) transform!(tscene; kw_args...) - tscene + tscene end function transform!( @@ -84,7 +55,7 @@ function transform!( end function transform!( - scene::Transformable, attributes::Union{Attributes, AbstractDict} + scene::Transformable, attributes::Union{Attributes, AbstractDict, NamedTuple} ) transform!(scene; attributes...) end diff --git a/src/lighting.jl b/src/lighting.jl new file mode 100644 index 00000000000..c4a0a814dd6 --- /dev/null +++ b/src/lighting.jl @@ -0,0 +1,278 @@ +abstract type AbstractLight end + +# GLMakie interface + +# These need to match up with light shaders to differentiate light types +module LightType + const UNDEFINED = 0 + const Ambient = 1 + const PointLight = 2 + const DirectionalLight = 3 + const SpotLight = 4 + const RectLight = 5 +end + +# Each light should implement +light_type(::AbstractLight) = LightType.UNDEFINED +light_color(::AbstractLight) = RGBf(0, 0, 0) +# Other attributes need to be handled explicitly in backends + + +""" + AmbientLight(color) <: AbstractLight + +A simple ambient light that uniformly lights every object based on its light color. + +Availability: +- All backends with `shading = FastShading` or `MultiLightShading` +""" +struct AmbientLight <: AbstractLight + color::Observable{RGBf} +end + +light_type(::AmbientLight) = LightType.Ambient +light_color(l::AmbientLight) = l.color[] + + +""" + PointLight(color, position[, attenuation = Vec2f(0)]) + PointLight(color, position, range::Real) + +A point-like light source placed at the given `position` with the given light +`color`. + +Optionally an attenuation parameter can be used to reduce the brightness of the +light source with distance. The reduction is given by +`1 / (1 + attenuation[1] * distance + attenuation[2] * distance^2)`. +Alternatively you can pass a light `range` to generate matching default +attenuation parameters. Note that you may need to set the light intensity, i.e. +the light color to values greater than 1 to get satisfying results. + +Availability: +- GLMakie with `shading = MultiLightShading` +- RPRMakie +""" +struct PointLight <: AbstractLight + color::Observable{RGBf} + position::Observable{Vec3f} + attenuation::Observable{Vec2f} +end + +# no attenuation +function PointLight(color::Union{Colorant, Observable{<: Colorant}}, position::Union{VecTypes{3}, Observable{<: VecTypes{3}}}) + return PointLight(color, position, Vec2f(0)) +end +# automatic attenuation +function PointLight(color::Union{Colorant, Observable{<: Colorant}}, position::Union{VecTypes{3}, Observable{<: VecTypes{3}}}, range::Real) + return PointLight(color, position, default_attenuation(range)) +end + +@deprecate PointLight(position::Union{VecTypes{3}, Observable{<: VecTypes{3}}}, color::Union{Colorant, Observable{<: Colorant}}) PointLight(color, position) + +light_type(::PointLight) = LightType.PointLight +light_color(l::PointLight) = l.color[] + +# fit of values used on learnopengl/ogre3d +function default_attenuation(range::Real) + return Vec2f( + 4.690507869767646 * range ^ -1.009712247799057, + 82.4447791934059 * range ^ -2.0192061630628966 + ) +end + + +""" + DirectionalLight(color, direction[, camera_relative = false]) + +A light type which simulates a distant light source with parallel light rays +going in the given `direction`. + +Availability: +- All backends with `shading = FastShading` or `MultiLightShading` +""" +struct DirectionalLight <: AbstractLight + color::Observable{RGBf} + direction::Observable{Vec3f} + + # Usually a light source is placed in world space, i.e. unrelated to the + # camera. As a default however, we want to make sure that an object is + # always reasonably lit, which requires the light source to move with the + # camera. To keep this in sync in WGLMakie, the calculation needs to happen + # in javascript. This flag notives WGLMakie and other backends that this + # calculation needs to happen. + camera_relative::Bool + + DirectionalLight(col, dir, rel = false) = new(col, dir, rel) +end +light_type(::DirectionalLight) = LightType.DirectionalLight +light_color(l::DirectionalLight) = l.color[] + + +""" + SpotLight(color, position, direction, angles) + +Creates a spot light which illuminates objects in a light cone starting at +`position` pointing in `direction`. The opening angle is defined by an inner +and outer angle given in `angles`, between which the light intensity drops off. + +Availability: +- GLMakie with `shading = MultiLightShading` +- RPRMakie +""" +struct SpotLight <: AbstractLight + color::Observable{RGBf} + position::Observable{Vec3f} + direction::Observable{Vec3f} + angles::Observable{Vec2f} +end + +light_type(::SpotLight) = LightType.SpotLight +light_color(l::SpotLight) = l.color[] + + +""" + EnvironmentLight(intensity, image) + +An environment light that uses a spherical environment map to provide lighting. +See: https://en.wikipedia.org/wiki/Reflection_mapping + +Availability: +- RPRMakie +""" +struct EnvironmentLight <: AbstractLight + intensity::Observable{Float32} + image::Observable{Matrix{RGBf}} +end + +""" + RectLight(color, r::Rect2[, direction = -normal]) + RectLight(color, center::Point3f, b1::Vec3f, b2::Vec3f[, direction = -normal]) + +Creates a RectLight with a given color. The first constructor derives the light +from a `Rect2` extending in x and y direction. The second specifies the `center` +of the rect (or more accurately parallelogram) with `b1` and `b2` specifying the +width and height vectors (including scale). + +Note that RectLight implements `translate!`, `rotate!` and `scale!` to simplify +adjusting the light. + +Availability: +- GLMakie with `Shading = MultiLightShading` +""" +struct RectLight <: AbstractLight + color::Observable{RGBf} + position::Observable{Point3f} + u1::Observable{Vec3f} + u2::Observable{Vec3f} + direction::Observable{Vec3f} +end + +RectLight(color, pos, u1, u2) = RectLight(color, pos, u1, u2, -normalize(cross(u1, u2))) +function RectLight(color, r::Rect2) + mini = minimum(r); ws = widths(r) + position = Observable(to_ndim(Point3f, mini + 0.5 * ws, 0)) + u1 = Observable(Vec3f(ws[1], 0, 0)) + u2 = Observable(Vec3f(0, ws[2], 0)) + return RectLight(color, position, u1, u2, normalize(Vec3f(0,0,-1))) +end + +# Implement Transformable interface (more or less) to simplify working with +# RectLights + +function translate!(::Type{T}, l::RectLight, v) where T + offset = to_ndim(Vec3f, Float32.(v), 0) + if T === Accum + l.position[] = l.position[] + offset + elseif T === Absolute + l.position[] = offset + else + error("Unknown translation type: $T") + end +end +translate!(l::RectLight, v) = translate!(Absolute, l, v) + +function rotate!(l::RectLight, q...) + rot = convert_attribute(q, key"rotation"()) + l.u1[] = rot * l.u1[] + l.u2[] = rot * l.u2[] + l.direction[] = rot * l.direction[] +end + +function scale!(::Type{T}, l::RectLight, s) where T + scale = to_ndim(Vec2f, Float32.(s), 0) + if T === Accum + l.u1[] = scale[1] * l.u1[] + l.u2[] = scale[2] * l.u2[] + elseif T === Absolute + l.u1[] = scale[1] * normalize(l.u1[]) + l.u2[] = scale[2] * normalize(l.u2[]) + else + error("Unknown translation type: $T") + end +end +scale!(l::RectLight, x::Real, y::Real) = scale!(Accum, l, Vec2f(x, y)) +scale!(l::RectLight, xy::VecTypes) = scale!(Accum, l, xy) + + +light_type(::RectLight) = LightType.RectLight +light_color(l::RectLight) = l.color[] + + +################################################################################ + + +function get_one_light(lights, Typ) + indices = findall(x-> x isa Typ, lights) + isempty(indices) && return nothing + return lights[indices[1]] +end + +function default_shading!(plot, lights::Vector{<: AbstractLight}) + # if the plot does not have :shading we assume the plot doesn't support it + haskey(plot.attributes, :shading) || return + + # Bad type + shading = to_value(plot.attributes[:shading]) + if !(shading isa MakieCore.ShadingAlgorithm || shading === automatic) + prev = shading + if (shading isa Bool) && (shading == false) + shading = NoShading + else + shading = automatic + end + @warn "`shading = $prev` is not valid. Use `automatic`, `NoShading`, `FastShading` or `MultiLightShading`. Defaulting to `$shading`." + end + + # automatic conversion + if shading === automatic + ambient_count = 0 + dir_light_count = 0 + + for light in lights + if light isa AmbientLight + ambient_count += 1 + elseif light isa DirectionalLight + dir_light_count += 1 + elseif light isa EnvironmentLight + continue + else + plot.attributes[:shading] = MultiLightShading + return + end + if ambient_count > 1 || dir_light_count > 1 + plot.attributes[:shading] = MultiLightShading + return + end + end + + if dir_light_count + ambient_count == 0 + shading = NoShading + else + shading = FastShading + end + end + + plot.attributes[:shading] = shading + + return +end \ No newline at end of file diff --git a/src/makielayout/MakieLayout.jl b/src/makielayout/MakieLayout.jl index c6fe466af8a..1cbe57bc478 100644 --- a/src/makielayout/MakieLayout.jl +++ b/src/makielayout/MakieLayout.jl @@ -67,7 +67,6 @@ export Cycled # from GridLayoutBase export GridLayout, GridPosition, GridSubposition -export GridLayoutSpec export BBox export LayoutObservables export Inside, Outside, Mixed @@ -92,5 +91,3 @@ export grid!, hgrid!, vgrid! export swap! export ncols, nrows export contents, content - -Base.@deprecate_binding MakieLayout Makie true "The module `MakieLayout` has been removed and integrated into Makie, so simply replace all usage of `MakieLayout` with `Makie`." diff --git a/src/makielayout/blocks.jl b/src/makielayout/blocks.jl index 855c28627ae..3b9d40f4d99 100644 --- a/src/makielayout/blocks.jl +++ b/src/makielayout/blocks.jl @@ -1,4 +1,4 @@ -abstract type Block end + function is_attribute end function default_attribute_values end @@ -6,13 +6,14 @@ function attribute_default_expressions end function _attribute_docs end function has_forwarded_layout end - -macro Block(name::Symbol, body::Expr = Expr(:block)) +macro Block(_name::Union{Expr, Symbol}, body::Expr = Expr(:block)) body.head === :block || error("A Block needs to be defined within a `begin end` block") + type_expr = _name isa Expr ? _name : :($_name <: Makie.Block) + name = _name isa Symbol ? _name : _name.args[1] structdef = quote - mutable struct $name <: Makie.Block + mutable struct $(type_expr) parent::Union{Figure, Scene, Nothing} layoutobservables::Makie.LayoutObservables{GridLayout} blockscene::Scene @@ -255,6 +256,7 @@ end can_be_current_axis(x) = false +get_top_parent(gp::GridLayout) = GridLayoutBase.top_parent(gp) get_top_parent(gp::GridPosition) = GridLayoutBase.top_parent(gp.layout) get_top_parent(gp::GridSubposition) = get_top_parent(gp.parent) @@ -269,50 +271,58 @@ function _block(T::Type{<:Block}, b end -function _block(T::Type{<:Block}, fig_or_scene::Union{Figure, Scene}, - args...; bbox = nothing, kwargs...) - # first sort out all user kwargs that correspond to block attributes - kwdict = Dict(kwargs) - - if haskey(kwdict, :textsize) - throw(ArgumentError("The attribute `textsize` has been renamed to `fontsize` in Makie v0.19. Please change all occurrences of `textsize` to `fontsize` or revert back to an earlier version.")) - end - - attribute_kwargs = Dict{Symbol, Any}() - for (key, value) in kwdict - if is_attribute(T, key) - attribute_kwargs[key] = pop!(kwdict, key) - end - end - # the non-attribute kwargs will be passed to the block later - non_attribute_kwargs = kwdict +function _block(T::Type{<:Block}, fig_or_scene::Union{Figure, Scene}, args...; bbox = nothing, kwargs...) + return _block(T, fig_or_scene, Any[args...], Dict{Symbol,Any}(kwargs), bbox) +end - topscene = get_topscene(fig_or_scene) - # retrieve the default attributes for this block given the scene theme - # and also the `Block = (...` style attributes from scene and global theme - default_attrs = default_attribute_values(T, topscene) - typekey_scene_attrs = get(theme(topscene), nameof(T), Attributes())::Attributes - typekey_attrs = theme(nameof(T); default=Attributes())::Attributes +function block_defaults(blockname::Symbol, attribute_kwargs::Dict, scene::Union{Nothing, Scene}) + default_attrs = default_attribute_values(getfield(Makie, blockname), scene) + typekey_scene_attrs = get(theme(scene), blockname, Attributes()) + typekey_attrs = theme(blockname; default=Attributes())::Attributes + attributes = Dict{Symbol,Any}() # make a final attribute dictionary using different priorities # for the different themes - attributes = Dict{Symbol, Any}() for (key, val) in default_attrs # give kwargs priority if haskey(attribute_kwargs, key) attributes[key] = attribute_kwargs[key] - # otherwise scene theme + # otherwise scene theme elseif haskey(typekey_scene_attrs, key) attributes[key] = typekey_scene_attrs[key] - # otherwise global theme + # otherwise global theme elseif haskey(typekey_attrs, key) attributes[key] = typekey_attrs[key] - # otherwise its the value from the type default theme + # otherwise its the value from the type default theme else attributes[key] = val end end + return attributes +end + +function _block(T::Type{<:Block}, fig_or_scene::Union{Figure,Scene}, args, kwdict::Dict, bbox; kwdict_complete=false) + + # first sort out all user kwargs that correspond to block attributes + check_textsize_deprecation(kwdict) + + attribute_kwargs = Dict{Symbol, Any}() + for (key, value) in kwdict + if is_attribute(T, key) + attribute_kwargs[key] = pop!(kwdict, key) + end + end + # the non-attribute kwargs will be passed to the block later + non_attribute_kwargs = kwdict + topscene = get_topscene(fig_or_scene) + # retrieve the default attributes for this block given the scene theme + # and also the `Block = (...` style attributes from scene and global theme + if kwdict_complete + attributes = attribute_kwargs + else + attributes = block_defaults(nameof(T), attribute_kwargs, topscene) + end # create basic layout observables and connect attribute observables further down # after creating the block with its observable fields @@ -404,6 +414,7 @@ end """ Get the scene which blocks need from their parent to plot stuff into """ +get_topscene(f::Union{GridPosition, GridSubposition}) = get_topscene(get_top_parent(f)) get_topscene(f::Figure) = f.scene function get_topscene(s::Scene) if !(Makie.cameracontrols(s) isa Makie.PixelCamera) @@ -465,7 +476,7 @@ free(::Block) = nothing function Base.delete!(block::Block) free(block) block.parent === nothing && return - # detach plots, cameras, transformations, px_area + # detach plots, cameras, transformations, viewport empty!(block.blockscene) gc = GridLayoutBase.gridcontent(block) @@ -509,22 +520,22 @@ end # if a non-observable is passed, its value is converted and placed into an observable of # the correct type which is then used as the block field -function init_observable!(@nospecialize(x), key, @nospecialize(OT), @nospecialize(value)) +function init_observable!(@nospecialize(block), key::Symbol, @nospecialize(OT), @nospecialize(value)) o = convert_for_attribute(observable_type(OT), value) - setfield!(x, key, OT(o)) - return x + setfield!(block, key, OT(o)) + return block end # if an observable is passed, a converted type is lifted off of it, so it is # not used directly as a block field -function init_observable!(@nospecialize(x), key, @nospecialize(OT), @nospecialize(value::Observable)) +function init_observable!(@nospecialize(block), key::Symbol, @nospecialize(OT), @nospecialize(value::Observable)) obstype = observable_type(OT) o = Observable{obstype}() map!(o, value) do v convert_for_attribute(obstype, v) end - setfield!(x, key, o) - return x + setfield!(block, key, o) + return block end observable_type(x::Type{Observable{T}}) where T = T diff --git a/src/makielayout/blocks/axis.jl b/src/makielayout/blocks/axis.jl index 7d4bd026c33..ccad1873578 100644 --- a/src/makielayout/blocks/axis.jl +++ b/src/makielayout/blocks/axis.jl @@ -158,17 +158,11 @@ function compute_protrusions(title, titlesize, titlegap, titlevisible, spinewidt end function initialize_block!(ax::Axis; palette = nothing) - blockscene = ax.blockscene elements = Dict{Symbol, Any}() ax.elements = elements - if palette === nothing - palette = fast_deepcopy(get(blockscene.theme, :palette, Makie.DEFAULT_PALETTES)) - end - ax.palette = palette isa Attributes ? palette : Attributes(palette) - # initialize either with user limits, or pick defaults based on scales # so that we don't immediately error targetlimits = Observable{Rect2f}(defaultlimits(ax.limits[], ax.xscale[], ax.yscale[])) @@ -176,8 +170,6 @@ function initialize_block!(ax::Axis; palette = nothing) setfield!(ax, :targetlimits, targetlimits) setfield!(ax, :finallimits, finallimits) - ax.cycler = Cycler() - on(blockscene, targetlimits) do lims # this should validate the targetlimits before anything else happens with them # so there should be nothing before this lifting `targetlimits` @@ -190,12 +182,18 @@ function initialize_block!(ax::Axis; palette = nothing) scenearea = sceneareanode!(ax.layoutobservables.computedbbox, finallimits, ax.aspect) - scene = Scene(blockscene, px_area=scenearea) + scene = Scene(blockscene, viewport=scenearea) ax.scene = scene + if !isnothing(palette) + # Backwards compatibility for when palette was part of axis! + palette_attr = palette isa Attributes ? palette : Attributes(palette) + ax.scene.theme.palette = palette_attr + end + # TODO: replace with mesh, however, CairoMakie needs a poly path for this signature # so it doesn't rasterize the scene - background = poly!(blockscene, scenearea; color=ax.backgroundcolor, inspectable=false, shading=false, strokecolor=:transparent) + background = poly!(blockscene, scenearea; color=ax.backgroundcolor, inspectable=false, shading=NoShading, strokecolor=:transparent) translate!(background, 0, 0, -100) elements[:background] = background @@ -259,7 +257,7 @@ function initialize_block!(ax::Axis; palette = nothing) # 3. Update the view onto the plot (camera matrices) onany(update_axis_camera, camera(scene), scene.transformation.transform_func, finallimits, ax.xreversed, ax.yreversed, priority = -2) - xaxis_endpoints = lift(blockscene, ax.xaxisposition, scene.px_area; + xaxis_endpoints = lift(blockscene, ax.xaxisposition, scene.viewport; ignore_equal_values=true) do xaxisposition, area if xaxisposition === :bottom return bottomline(Rect2f(area)) @@ -270,7 +268,7 @@ function initialize_block!(ax::Axis; palette = nothing) end end - yaxis_endpoints = lift(blockscene, ax.yaxisposition, scene.px_area; + yaxis_endpoints = lift(blockscene, ax.yaxisposition, scene.viewport; ignore_equal_values=true) do yaxisposition, area if yaxisposition === :left return leftline(Rect2f(area)) @@ -347,7 +345,7 @@ function initialize_block!(ax::Axis; palette = nothing) ax.yaxis = yaxis - xoppositelinepoints = lift(blockscene, scene.px_area, ax.spinewidth, ax.xaxisposition; + xoppositelinepoints = lift(blockscene, scene.viewport, ax.spinewidth, ax.xaxisposition; ignore_equal_values=true) do r, sw, xaxpos if xaxpos === :top y = bottom(r) @@ -362,7 +360,7 @@ function initialize_block!(ax::Axis; palette = nothing) end end - yoppositelinepoints = lift(blockscene, scene.px_area, ax.spinewidth, ax.yaxisposition; + yoppositelinepoints = lift(blockscene, scene.viewport, ax.spinewidth, ax.yaxisposition; ignore_equal_values=true) do r, sw, yaxpos if yaxpos === :right x = left(r) @@ -378,22 +376,22 @@ function initialize_block!(ax::Axis; palette = nothing) end xticksmirrored = lift(mirror_ticks, blockscene, xaxis.tickpositions, ax.xticksize, ax.xtickalign, - Ref(scene.px_area), :x, ax.xaxisposition[]) + Ref(scene.viewport), :x, ax.xaxisposition[]) xticksmirrored_lines = linesegments!(blockscene, xticksmirrored, visible = @lift($(ax.xticksmirrored) && $(ax.xticksvisible)), linewidth = ax.xtickwidth, color = ax.xtickcolor) translate!(xticksmirrored_lines, 0, 0, 10) yticksmirrored = lift(mirror_ticks, blockscene, yaxis.tickpositions, ax.yticksize, ax.ytickalign, - Ref(scene.px_area), :y, ax.yaxisposition[]) + Ref(scene.viewport), :y, ax.yaxisposition[]) yticksmirrored_lines = linesegments!(blockscene, yticksmirrored, visible = @lift($(ax.yticksmirrored) && $(ax.yticksvisible)), linewidth = ax.ytickwidth, color = ax.ytickcolor) translate!(yticksmirrored_lines, 0, 0, 10) xminorticksmirrored = lift(mirror_ticks, blockscene, xaxis.minortickpositions, ax.xminorticksize, - ax.xminortickalign, Ref(scene.px_area), :x, ax.xaxisposition[]) + ax.xminortickalign, Ref(scene.viewport), :x, ax.xaxisposition[]) xminorticksmirrored_lines = linesegments!(blockscene, xminorticksmirrored, visible = @lift($(ax.xticksmirrored) && $(ax.xminorticksvisible)), linewidth = ax.xminortickwidth, color = ax.xminortickcolor) translate!(xminorticksmirrored_lines, 0, 0, 10) yminorticksmirrored = lift(mirror_ticks, blockscene, yaxis.minortickpositions, ax.yminorticksize, - ax.yminortickalign, Ref(scene.px_area), :y, ax.yaxisposition[]) + ax.yminortickalign, Ref(scene.viewport), :y, ax.yaxisposition[]) yminorticksmirrored_lines = linesegments!(blockscene, yminorticksmirrored, visible = @lift($(ax.yticksmirrored) && $(ax.yminorticksvisible)), linewidth = ax.yminortickwidth, color = ax.yminortickcolor) translate!(yminorticksmirrored_lines, 0, 0, 10) @@ -410,31 +408,31 @@ function initialize_block!(ax::Axis; palette = nothing) elements[:yoppositeline] = yoppositeline translate!(yoppositeline, 0, 0, 20) - onany(blockscene, xaxis.tickpositions, scene.px_area) do tickpos, area + onany(blockscene, xaxis.tickpositions, scene.viewport) do tickpos, area local pxheight::Float32 = height(area) local offset::Float32 = ax.xaxisposition[] === :bottom ? pxheight : -pxheight update_gridlines!(xgridnode, Point2f(0, offset), tickpos) end - onany(blockscene, yaxis.tickpositions, scene.px_area) do tickpos, area + onany(blockscene, yaxis.tickpositions, scene.viewport) do tickpos, area local pxwidth::Float32 = width(area) local offset::Float32 = ax.yaxisposition[] === :left ? pxwidth : -pxwidth update_gridlines!(ygridnode, Point2f(offset, 0), tickpos) end - onany(blockscene, xaxis.minortickpositions, scene.px_area) do tickpos, area - local pxheight::Float32 = height(scene.px_area[]) + onany(blockscene, xaxis.minortickpositions, scene.viewport) do tickpos, area + local pxheight::Float32 = height(scene.viewport[]) local offset::Float32 = ax.xaxisposition[] === :bottom ? pxheight : -pxheight update_gridlines!(xminorgridnode, Point2f(0, offset), tickpos) end - onany(blockscene, yaxis.minortickpositions, scene.px_area) do tickpos, area - local pxwidth::Float32 = width(scene.px_area[]) + onany(blockscene, yaxis.minortickpositions, scene.viewport) do tickpos, area + local pxwidth::Float32 = width(scene.viewport[]) local offset::Float32 = ax.yaxisposition[] === :left ? pxwidth : -pxwidth update_gridlines!(yminorgridnode, Point2f(offset, 0), tickpos) end - subtitlepos = lift(blockscene, scene.px_area, ax.titlegap, ax.titlealign, ax.xaxisposition, + subtitlepos = lift(blockscene, scene.viewport, ax.titlegap, ax.titlealign, ax.xaxisposition, xaxis.protrusion; ignore_equal_values=true) do a, titlegap, align, xaxisposition, xaxisprotrusion @@ -463,7 +461,7 @@ function initialize_block!(ax::Axis; palette = nothing) markerspace = :data, inspectable = false) - titlepos = lift(calculate_title_position, blockscene, scene.px_area, ax.titlegap, ax.subtitlegap, + titlepos = lift(calculate_title_position, blockscene, scene.viewport, ax.titlegap, ax.subtitlegap, ax.titlealign, ax.xaxisposition, xaxis.protrusion, ax.subtitlelineheight, ax, subtitlet; ignore_equal_values=true) titlet = text!( @@ -506,7 +504,7 @@ function initialize_block!(ax::Axis; palette = nothing) # compute limits that adhere to the limit aspect ratio whenever the targeted # limits or the scene size change, because both influence the displayed ratio - onany(blockscene, scene.px_area, targetlimits) do pxa, lims + onany(blockscene, scene.viewport, targetlimits) do pxa, lims adjustlimits!(ax) end @@ -522,8 +520,8 @@ function initialize_block!(ax::Axis; palette = nothing) return ax end -function mirror_ticks(tickpositions, ticksize, tickalign, px_area, side, axisposition) - a = px_area[][] +function mirror_ticks(tickpositions, ticksize, tickalign, viewport, side, axisposition) + a = viewport[][] if side === :x opp = axisposition === :bottom ? top(a) : bottom(a) sign = axisposition === :bottom ? 1 : -1 @@ -651,7 +649,6 @@ end function convert_limit_attribute(lims::Tuple{Any, Any}) lims end -can_be_current_axis(ax::Axis) = true function validate_limits_for_scales(lims::Rect, xsc, ysc) mi = minimum(lims) @@ -675,61 +672,55 @@ attrsyms(cycle::Cycle) = [c[1] for c in cycle.cycle] function get_cycler_index!(c::Cycler, P::Type) if !haskey(c.counters, P) - c.counters[P] = 1 + return c.counters[P] = 1 else - c.counters[P] += 1 + return c.counters[P] += 1 end end -function get_cycle_for_plottype(allattrs, P)::Cycle - psym = MakieCore.plotsym(P) - - plottheme = Makie.default_theme(nothing, P) - - cycle_raw = if haskey(allattrs, :cycle) - allattrs.cycle[] - else - global_theme_cycle = theme(psym) - if !isnothing(global_theme_cycle) && haskey(global_theme_cycle, :cycle) - global_theme_cycle.cycle[] - else - haskey(plottheme, :cycle) ? plottheme.cycle[] : nothing - end - end - +function get_cycle_for_plottype(cycle_raw)::Cycle if isnothing(cycle_raw) - Cycle([]) + return Cycle([]) elseif cycle_raw isa Cycle - cycle_raw + return cycle_raw else - Cycle(cycle_raw) + return Cycle(cycle_raw) end end -function add_cycle_attributes!(allattrs, P, cycle::Cycle, cycler::Cycler, palette::Attributes) +function to_color(scene::Scene, attribute_name, cycled::Cycled) + palettes = to_value(scene.theme.palette) + attr_palette = to_value(palettes[attribute_name]) + index = cycled.i + return attr_palette[mod1(index, length(attr_palette))] +end + +function add_cycle_attributes!(@nospecialize(plot), cycle::Cycle, cycler::Cycler, palette::Attributes) # check if none of the cycled attributes of this plot # were passed manually, because we don't use the cycler # if any of the cycled attributes were specified manually - no_cycle_attribute_passed = !any(keys(allattrs)) do key + user_attributes = plot.kw + no_cycle_attribute_passed = !any(keys(user_attributes)) do key any(syms -> key in syms, attrsyms(cycle)) end # check if any attributes were passed as `Cycled` entries # because if there were any, these are looked up directly # in the cycler without advancing the counter etc. - manually_cycled_attributes = filter(keys(allattrs)) do key - to_value(allattrs[key]) isa Cycled + manually_cycled_attributes = filter(keys(user_attributes)) do key + return to_value(user_attributes[key]) isa Cycled end # if there are any manually cycled attributes, we don't do the normal # cycling but only look up exactly the passed attributes cycle_attrsyms = attrsyms(cycle) + if !isempty(manually_cycled_attributes) # an attribute given as Cycled needs to be present in the cycler, # otherwise there's no cycle in which to look up a value for k in manually_cycled_attributes if !any(x -> k in x, cycle_attrsyms) - error("Attribute `$k` was passed with an explicit `Cycled` value, but $k is not specified in the cycler for this plot type $P.") + error("Attribute `$k` was passed with an explicit `Cycled` value, but $k is not specified in the cycler for this plot type $(typeof(plot)).") end end @@ -737,10 +728,10 @@ function add_cycle_attributes!(allattrs, P, cycle::Cycle, cycler::Cycler, palett for sym in manually_cycled_attributes isym = findfirst(syms -> sym in syms, attrsyms(cycle)) - index = allattrs[sym][].i + index = plot[sym][].i # replace the Cycled values with values from the correct palettes # at the index inside the Cycled object - allattrs[sym] = if cycle.covary + plot[sym] = if cycle.covary palettes[isym][mod1(index, length(palettes[isym]))] else cis = CartesianIndices(Tuple(length(p) for p in palettes)) @@ -752,13 +743,13 @@ function add_cycle_attributes!(allattrs, P, cycle::Cycle, cycler::Cycler, palett end elseif no_cycle_attribute_passed - index = get_cycler_index!(cycler, P) + index = get_cycler_index!(cycler, typeof(plot)) palettes = [palette[sym][] for sym in palettesyms(cycle)] for (isym, syms) in enumerate(attrsyms(cycle)) for sym in syms - allattrs[sym] = if cycle.covary + plot[sym] = if cycle.covary palettes[isym][mod1(index, length(palettes[isym]))] else cis = CartesianIndices(Tuple(length(p) for p in palettes)) @@ -772,37 +763,10 @@ function add_cycle_attributes!(allattrs, P, cycle::Cycle, cycler::Cycler, palett end end -function Makie.plot!( - la::Axis, P::Makie.PlotFunc, - attributes::Makie.Attributes, args...; - kw_attributes...) - - allattrs = merge(attributes, Attributes(kw_attributes)) - - _disallow_keyword(:axis, allattrs) - _disallow_keyword(:figure, allattrs) - cycle = get_cycle_for_plottype(allattrs, P) - add_cycle_attributes!(allattrs, P, cycle, la.cycler, la.palette) - - plot = Makie.plot!(la.scene, P, allattrs, args...) - - # some area-like plots basically always look better if they cover the whole plot area. - # adjust the limit margins in those cases automatically. - needs_tight_limits(plot) && tightlimits!(la) - - if is_open_or_any_parent(la.scene) - reset_limits!(la) - end - plot -end - is_open_or_any_parent(s::Scene) = isopen(s) || is_open_or_any_parent(s.parent) is_open_or_any_parent(::Nothing) = false -function Makie.plot!(P::Makie.PlotFunc, ax::Axis, args...; kw_attributes...) - attributes = Makie.Attributes(kw_attributes) - Makie.plot!(ax, P, attributes, args...) -end + needs_tight_limits(@nospecialize any) = false needs_tight_limits(::Union{Heatmap, Image}) = true @@ -991,7 +955,7 @@ end function adjustlimits!(la) asp = la.autolimitaspect[] target = la.targetlimits[] - area = la.scene.px_area[] + area = la.scene.viewport[] # in the simplest case, just update the final limits with the target limits if isnothing(asp) || width(area) == 0 || height(area) == 0 @@ -1404,18 +1368,6 @@ function limits!(args...) limits!(current_axis(), args...) end -function Base.delete!(ax::Axis, plot::AbstractPlot) - delete!(ax.scene, plot) - ax -end - -function Base.empty!(ax::Axis) - while !isempty(ax.scene.plots) - delete!(ax, ax.scene.plots[end]) - end - ax -end - Makie.transform_func(ax::Axis) = Makie.transform_func(ax.scene) # these functions pick limits for different x and y scales, so that @@ -1452,10 +1404,6 @@ defined_interval(::LogFunctions) = OpenInterval(0.0, Inf) defined_interval(::typeof(sqrt)) = Interval{:closed,:open}(0, Inf) defined_interval(::typeof(Makie.logit)) = OpenInterval(0.0, 1.0) -function update_state_before_display!(ax::Axis) - reset_limits!(ax) - return -end function attribute_examples(::Type{Axis}) Dict( @@ -1892,7 +1840,7 @@ function colorbuffer(ax::Axis; include_decorations=true, update=true, colorbuffe bb = axis_bounds_with_decoration(ax) Rect2{Int}(round.(Int, minimum(bb)) .+ 1, round.(Int, widths(bb))) else - pixelarea(ax.scene)[] + viewport(ax.scene)[] end img = colorbuffer(root(ax.scene); update=false, colorbuffer_kws...) diff --git a/src/makielayout/blocks/axis3d.jl b/src/makielayout/blocks/axis3d.jl index 1717d2f3cfe..ec72eb0dce7 100644 --- a/src/makielayout/blocks/axis3d.jl +++ b/src/makielayout/blocks/axis3d.jl @@ -38,12 +38,13 @@ function initialize_block!(ax::Axis3) return end - matrices = lift(calculate_matrices, scene, finallimits, scene.px_area, ax.elevation, ax.azimuth, + matrices = lift(calculate_matrices, scene, finallimits, scene.viewport, ax.elevation, ax.azimuth, ax.perspectiveness, ax.aspect, ax.viewmode, ax.xreversed, ax.yreversed, ax.zreversed) - on(scene, matrices) do (view, proj, eyepos) + on(scene, matrices) do (model, view, proj, eyepos) cam = camera(scene) Makie.set_proj_view!(cam, proj, view) + scene.transformation.model[] = model cam.eyeposition[] = eyepos end @@ -80,7 +81,7 @@ function initialize_block!(ax::Axis3) zticks, zticklabels, zlabel = add_ticks_and_ticklabels!(blockscene, scene, ax, 3, finallimits, ticknode_3, mi3, mi1, mi2, ax.azimuth, ax.xreversed, ax.yreversed, ax.zreversed) - titlepos = lift(scene, scene.px_area, ax.titlegap, ax.titlealign) do a, titlegap, align + titlepos = lift(scene, scene.viewport, ax.titlegap, ax.titlealign) do a, titlegap, align align_factor = halign2num(align, "Horizontal title align $align not supported.") x = a.origin[1] + align_factor * a.widths[1] @@ -105,9 +106,6 @@ function initialize_block!(ax::Axis3) markerspace = :data, inspectable = false) - ax.cycler = Cycler() - ax.palette = Makie.DEFAULT_PALETTES - ax.mouseeventhandle = addmouseevents!(scene) scrollevents = Observable(ScrollEvent(0, 0)) setfield!(ax, :scrollevents, scrollevents) @@ -164,9 +162,7 @@ function initialize_block!(ax::Axis3) return end -can_be_current_axis(ax3::Axis3) = true - -function calculate_matrices(limits, px_area, elev, azim, perspectiveness, aspect, +function calculate_matrices(limits, viewport, elev, azim, perspectiveness, aspect, viewmode, xreversed, yreversed, zreversed) ori = limits.origin @@ -199,7 +195,7 @@ function calculate_matrices(limits, px_area, elev, azim, perspectiveness, aspect end |> Makie.scalematrix t2 = Makie.translationmatrix(-0.5 .* ws .* scales) - scale_matrix = t2 * s * t + model = t2 * s * t ang_max = 90 ang_min = 0.5 @@ -220,23 +216,16 @@ function calculate_matrices(limits, px_area, elev, azim, perspectiveness, aspect eyepos = Vec3{Float64}(x, y, z) - lookat_matrix = Makie.lookat( - eyepos, - Vec3{Float64}(0, 0, 0), - Vec3{Float64}(0, 0, 1)) - - w = width(px_area) - h = height(px_area) + lookat_matrix = lookat(eyepos, Vec3{Float64}(0), Vec3{Float64}(0, 0, 1)) - view_matrix = lookat_matrix * scale_matrix + w = width(viewport) + h = height(viewport) - projection_matrix = projectionmatrix(view_matrix, limits, eyepos, radius, azim, elev, angle, w, h, scales, viewmode) + projection_matrix = projectionmatrix( + lookat_matrix * model, limits, eyepos, radius, azim, elev, angle, + w, h, scales, viewmode) - # for eyeposition dependent algorithms, we need to present the position as if - # there was no scaling applied - eyeposition = Vec3f(inv(scale_matrix) * Vec4f(eyepos..., 1)) - - view_matrix, projection_matrix, eyeposition + return model, lookat_matrix, projection_matrix, eyepos end function projectionmatrix(viewmatrix, limits, eyepos, radius, azim, elev, angle, width, height, scales, viewmode) @@ -278,33 +267,6 @@ function projectionmatrix(viewmatrix, limits, eyepos, radius, azim, elev, angle, end end - -function Makie.plot!( - ax::Axis3, P::Makie.PlotFunc, - attributes::Makie.Attributes, args...; - kw_attributes...) - - allattrs = merge(attributes, Attributes(kw_attributes)) - - _disallow_keyword(:axis, allattrs) - _disallow_keyword(:figure, allattrs) - - cycle = get_cycle_for_plottype(allattrs, P) - add_cycle_attributes!(allattrs, P, cycle, ax.cycler, ax.palette) - - plot = Makie.plot!(ax.scene, P, allattrs, args...) - - if is_open_or_any_parent(ax.scene) - reset_limits!(ax) - end - plot -end - -function Makie.plot!(P::Makie.PlotFunc, ax::Axis3, args...; kw_attributes...) - attributes = Makie.Attributes(kw_attributes) - Makie.plot!(ax, P, attributes, args...) -end - function update_state_before_display!(ax::Axis3) reset_limits!(ax) return @@ -358,10 +320,6 @@ function getlimits(ax::Axis3, dim) templim end -# mutable struct LineAxis3D - -# end - function dimpoint(dim, v, v1, v2) if dim == 1 Point(v, v1, v2) @@ -439,7 +397,7 @@ function add_gridlines_and_frames!(topscene, scene, ax, dim::Int, limits, tickno visible = attr(:gridvisible), inspectable = false) - framepoints = lift(limits, scene.camera.projectionview, scene.px_area, min1, min2, xreversed, yreversed, zreversed + framepoints = lift(limits, scene.camera.projectionview, scene.viewport, min1, min2, xreversed, yreversed, zreversed ) do lims, _, pxa, mi1, mi2, xrev, yrev, zrev o = pxa.origin @@ -476,7 +434,7 @@ end # this function projects a point from a 3d subscene into the parent space with a really # small z value function to_topscene_z_2d(p3d, scene) - o = scene.px_area[].origin + o = scene.viewport[].origin p2d = Point2f(o + Makie.project(scene, p3d)) # -10000 is an arbitrary weird constant that in preliminary testing didn't seem # to clip into plot objects anymore @@ -500,7 +458,7 @@ function add_ticks_and_ticklabels!(topscene, scene, ax, dim::Int, limits, tickno ticksize = attr(:ticksize) tick_segments = lift(topscene, limits, tickvalues, miv, min1, min2, - scene.camera.projectionview, scene.px_area, ticksize, xreversed, yreversed, zreversed) do lims, ticks, miv, min1, min2, + scene.camera.projectionview, scene.viewport, ticksize, xreversed, yreversed, zreversed) do lims, ticks, miv, min1, min2, pview, pxa, tsize, xrev, yrev, zrev rev1 = (xrev, yrev, zrev)[d1] @@ -540,7 +498,7 @@ function add_ticks_and_ticklabels!(topscene, scene, ax, dim::Int, limits, tickno # we are going to transform the 3d tick segments into 2d of the topscene # because otherwise they # be cut when they extend beyond the scene boundary - tick_segments_2dz = lift(topscene, tick_segments, scene.camera.projectionview, scene.px_area) do ts, _, _ + tick_segments_2dz = lift(topscene, tick_segments, scene.camera.projectionview, scene.viewport) do ts, _, _ map(ts) do p1_p2 to_topscene_z_2d.(p1_p2, Ref(scene)) end @@ -555,7 +513,7 @@ function add_ticks_and_ticklabels!(topscene, scene, ax, dim::Int, limits, tickno translate!(ticks, 0, 0, -10000) labels_positions = Observable{Any}() - map!(topscene, labels_positions, scene.px_area, scene.camera.projectionview, + map!(topscene, labels_positions, scene.viewport, scene.camera.projectionview, tick_segments, ticklabels, attr(:ticklabelpad)) do pxa, pv, ticksegs, ticklabs, pad o = pxa.origin @@ -591,7 +549,7 @@ function add_ticks_and_ticklabels!(topscene, scene, ax, dim::Int, limits, tickno label_align = Observable((:center, :top)) onany(topscene, - scene.px_area, scene.camera.projectionview, limits, miv, min1, min2, + scene.viewport, scene.camera.projectionview, limits, miv, min1, min2, attr(:labeloffset), attr(:labelrotation), attr(:labelalign), xreversed, yreversed, zreversed ) do pxa, pv, lims, miv, min1, min2, labeloffset, lrotation, lalign, xrev, yrev, zrev @@ -719,7 +677,7 @@ function add_panel!(scene, ax, dim1, dim2, dim3, limits, min3) faces = [1 2 3; 3 4 1] - panel = mesh!(scene, vertices, faces, shading = false, inspectable = false, + panel = mesh!(scene, vertices, faces, shading = NoShading, inspectable = false, xautolimits = false, yautolimits = false, zautolimits = false, color = attr(:panelcolor), visible = attr(:panelvisible)) return panel @@ -1165,3 +1123,8 @@ function attribute_examples(::Type{Axis3}) ] ) end + + +# Axis interface + +tightlimits!(ax::Axis3) = nothing # TODO, not implemented yet diff --git a/src/makielayout/blocks/colorbar.jl b/src/makielayout/blocks/colorbar.jl index ca2fb97da30..3c1d11f54f2 100644 --- a/src/makielayout/blocks/colorbar.jl +++ b/src/makielayout/blocks/colorbar.jl @@ -57,7 +57,7 @@ function extract_colormap(plot::Union{Arrows, StreamPlot}) return extract_colormap(plot.plots[1]) end -function extract_colormap(plot::Combined{volumeslices}) +function extract_colormap(plot::Plot{volumeslices}) return extract_colormap(plot.plots[1]) end @@ -101,10 +101,10 @@ function Colorbar(fig_or_scene, plot::AbstractPlot; kwargs...) func = plotfunc(plot) if isnothing(cmap) error("Neither $(func) nor any of its children use a colormap. Cannot create a Colorbar from this plot, please create it manually. - If this is a recipe, one needs to overload `Makie.extract_colormap(::$(Combined{func}))` to allow for the automatical creation of a Colorbar.") + If this is a recipe, one needs to overload `Makie.extract_colormap(::$(Plot{func}))` to allow for the automatical creation of a Colorbar.") end if !(cmap isa ColorMapping) - error("extract_colormap(::$(Combined{func})) returned an invalid value: $cmap. Needs to return either a `Makie.ColorMapping`.") + error("extract_colormap(::$(Plot{func})) returned an invalid value: $cmap. Needs to return either a `Makie.ColorMapping`.") end if to_value(cmap.color) isa Union{AbstractVector{<: Colorant}, Colorant} @@ -253,7 +253,6 @@ function initialize_block!(cb::Colorbar) show_cats[] = true end end - heatmap!(blockscene, xrange, yrange, continous_pixels; colormap=colormap, @@ -411,11 +410,13 @@ function initialize_block!(cb::Colorbar) # trigger protrusions with one of the attributes notify(cb.vertical) # We set everything via the ColorMapping now. To be backwards compatible, we always set those fields: - setfield!(cb, :limits, convert(Observable{Any}, limits)) - setfield!(cb, :colormap, convert(Observable{Any}, cmap.colormap)) - setfield!(cb, :highclip, convert(Observable{Any}, cmap.highclip)) - setfield!(cb, :lowclip, convert(Observable{Any}, cmap.lowclip)) - setfield!(cb, :scale, convert(Observable{Any}, cmap.scale)) + if (cb.colormap[] isa ColorMapping) + setfield!(cb, :limits, convert(Observable{Any}, limits)) + setfield!(cb, :colormap, convert(Observable{Any}, cmap.colormap)) + setfield!(cb, :highclip, convert(Observable{Any}, cmap.highclip)) + setfield!(cb, :lowclip, convert(Observable{Any}, cmap.lowclip)) + setfield!(cb, :scale, convert(Observable{Any}, cmap.scale)) + end # trigger bbox notify(cb.layoutobservables.suggestedbbox) notify(barbox) diff --git a/src/makielayout/blocks/intervalslider.jl b/src/makielayout/blocks/intervalslider.jl index d5e850ac059..01c456ca210 100644 --- a/src/makielayout/blocks/intervalslider.jl +++ b/src/makielayout/blocks/intervalslider.jl @@ -88,7 +88,7 @@ function initialize_block!(isl::IntervalSlider) end endbuttons = scatter!(blockscene, endpoints, color = endbuttoncolors, - markersize = isl.linewidth, strokewidth = 0, inspectable = false) + markersize = isl.linewidth, strokewidth = 0, inspectable = false, marker = Circle) linesegs = linesegments!(blockscene, linepoints, color = linecolors, linewidth = isl.linewidth, inspectable = false) @@ -107,7 +107,7 @@ function initialize_block!(isl::IntervalSlider) end buttonsizes = @lift($(isl.linewidth) .* $button_magnifications) buttons = scatter!(blockscene, middlepoints, color = isl.color_active, strokewidth = 0, - markersize = buttonsizes, inspectable = false) + markersize = buttonsizes, inspectable = false, marker = Circle) mouseevents = addmouseevents!(blockscene, isl.layoutobservables.computedbbox) diff --git a/src/makielayout/blocks/label.jl b/src/makielayout/blocks/label.jl index a755cc54e05..adc7de7fbc7 100644 --- a/src/makielayout/blocks/label.jl +++ b/src/makielayout/blocks/label.jl @@ -11,12 +11,13 @@ function initialize_block!(l::Label) t = text!( topscene, textpos, text = l.text, fontsize = l.fontsize, font = l.font, color = l.color, visible = l.visible, align = (:center, :center), rotation = l.rotation, markerspace = :data, - justification = l.justification, lineheight = l.lineheight, word_wrap_width = word_wrap_width, + justification = l.justification, lineheight = l.lineheight, word_wrap_width = word_wrap_width, inspectable = false) textbb = Ref(BBox(0, 1, 0, 1)) - onany(l.text, l.fontsize, l.font, l.rotation, word_wrap_width, l.padding) do _, _, _, _, _, padding + onany(topscene, l.text, l.fontsize, l.font, l.rotation, word_wrap_width, + l.padding) do _, _, _, _, _, padding textbb[] = Rect2f(boundingbox(t)) autowidth = width(textbb[]) + padding[1] + padding[2] autoheight = height(textbb[]) + padding[3] + padding[4] @@ -28,7 +29,7 @@ function initialize_block!(l::Label) return end - onany(layoutobservables.computedbbox, l.padding) do bbox, padding + onany(topscene, layoutobservables.computedbbox, l.padding) do bbox, padding if l.word_wrap[] tw = width(bbox) - padding[1] - padding[2] else diff --git a/src/makielayout/blocks/legend.jl b/src/makielayout/blocks/legend.jl index 6f7492b476c..8d9310f8ba4 100644 --- a/src/makielayout/blocks/legend.jl +++ b/src/makielayout/blocks/legend.jl @@ -1,6 +1,5 @@ -function initialize_block!(leg::Legend, - entry_groups::Observable{Vector{Tuple{Any, Vector{LegendEntry}}}}) - +function initialize_block!(leg::Legend; entrygroups) + entry_groups = convert(Observable{Vector{Tuple{Any,Vector{LegendEntry}}}}, entrygroups) blockscene = leg.blockscene # by default, `tellwidth = true` and `tellheight = false` for vertical legends @@ -12,7 +11,7 @@ function initialize_block!(leg::Legend, legend_area = lift(round_to_IRect2D, blockscene, leg.layoutobservables.computedbbox) - scene = Scene(blockscene, blockscene.px_area, camera = campixel!) + scene = Scene(blockscene, blockscene.viewport, camera = campixel!) # the rectangle in which the legend is drawn when margins are removed legendrect = lift(blockscene, legend_area, leg.margin) do la, lm @@ -468,7 +467,24 @@ function Base.propertynames(legendelement::T) where T <: LegendElement [fieldnames(T)..., keys(legendelement.attributes)...] end +function to_entry_group(legend_defaults, contents::AbstractVector, labels::AbstractVector, title=nothing) + if length(contents) != length(labels) + error("Number of elements not equal: $(length(contents)) content elements and $(length(labels)) labels.") + end + entries = [LegendEntry(label, content, legend_defaults) for (content, label) in zip(contents, labels)] + return [(title, entries)] +end +function to_entry_group( + legend_defaults, contentgroups::AbstractVector{<:AbstractVector}, + labelgroups::AbstractVector{<:AbstractVector}, titles::AbstractVector) + if !(length(titles) == length(contentgroups) == length(labelgroups)) + error("Number of elements not equal: $(length(titles)) titles, $(length(contentgroups)) content groups and $(length(labelgroups)) label groups.") + end + entries = [[LegendEntry(l, pg, legend_defaults) for (l, pg) in zip(labelgroup, contentgroup)] + for (labelgroup, contentgroup) in zip(labelgroups, contentgroups)] + return [(t, en) for (t, en) in zip(titles, entries)] +end """ Legend( @@ -487,17 +503,15 @@ function Legend(fig_or_scene, contents::AbstractVector, labels::AbstractVector, title = nothing; - kwargs...) + bbox=nothing, kwargs...) - if length(contents) != length(labels) - error("Number of elements not equal: $(length(contents)) content elements and $(length(labels)) labels.") - end - - entrygroups = Observable{Vector{EntryGroup}}([]) - legend = Legend(fig_or_scene, entrygroups; kwargs...) - entries = [LegendEntry(label, content, legend) for (content, label) in zip(contents, labels)] - entrygroups[] = [(title, entries)] - legend + scene = get_topscene(fig_or_scene) + legend_defaults = block_defaults(:Legend, Dict{Symbol, Any}(kwargs), scene) + entry_groups = to_entry_group(Attributes(legend_defaults), contents, labels, title) + entrygroups = Observable(entry_groups) + legend_defaults[:entrygroups] = entrygroups + # Use low-level constructor to not calculate legend_defaults a second time + return _block(Legend, fig_or_scene, (), legend_defaults, bbox; kwdict_complete=true) end @@ -522,19 +536,14 @@ function Legend(fig_or_scene, contentgroups::AbstractVector{<:AbstractVector}, labelgroups::AbstractVector{<:AbstractVector}, titles::AbstractVector; - kwargs...) + bbox=nothing, kwargs...) - if !(length(titles) == length(contentgroups) == length(labelgroups)) - error("Number of elements not equal: $(length(titles)) titles, $(length(contentgroups)) content groups and $(length(labelgroups)) label groups.") - end - - - entrygroups = Observable{Vector{EntryGroup}}([]) - legend = Legend(fig_or_scene, entrygroups; kwargs...) - entries = [[LegendEntry(l, pg, legend) for (l, pg) in zip(labelgroup, contentgroup)] - for (labelgroup, contentgroup) in zip(labelgroups, contentgroups)] - entrygroups[] = [(t, en) for (t, en) in zip(titles, entries)] - legend + scene = get_scene(fig_or_scene) + legend_defaults = block_defaults(:Legend, Dict{Symbol,Any}(kwargs), scene) + entry_groups = to_entry_group(legend_defaults, contentgroups, labelgroups, titles) + entrygroups = Observable(entry_groups) + legend_defaults[:entrygroups] = entrygroups + return _block(Legend, fig_or_scene, (), legend_defaults, bbox; kwdict_complete=true) end @@ -616,8 +625,8 @@ to one occurrence. """ function axislegend(ax, args...; position = :rt, kwargs...) Legend(ax.parent, args...; - bbox = ax.scene.px_area, - margin = (10, 10, 10, 10), + bbox = ax.scene.viewport, + margin = (6, 6, 6, 6), legend_position_to_aligns(position)..., kwargs...) end diff --git a/src/makielayout/blocks/menu.jl b/src/makielayout/blocks/menu.jl index d114ccb022a..a88471351ff 100644 --- a/src/makielayout/blocks/menu.jl +++ b/src/makielayout/blocks/menu.jl @@ -59,7 +59,7 @@ function initialize_block!(m::Menu; default = 1) map!(blockscene, _direction, m.layoutobservables.computedbbox, m.direction) do bb, dir if dir == Makie.automatic - pxa = pixelarea(blockscene)[] + pxa = viewport(blockscene)[] bottomspace = abs(bottom(pxa) - bottom(bb)) topspace = abs(top(pxa) - top(bb)) # slight preference for down @@ -81,7 +81,7 @@ function initialize_block!(m::Menu; default = 1) left(bbox), right(bbox), d === :down ? max(0, bottom(bbox) - h) : top(bbox), - d === :down ? bottom(bbox) : min(top(bbox) + h, top(blockscene.px_area[])))) + d === :down ? bottom(bbox) : min(top(bbox) + h, top(blockscene.viewport[])))) end menuscene = Scene(blockscene, scenearea, camera = campixel!, clear=true) @@ -256,7 +256,7 @@ function initialize_block!(m::Menu; default = 1) m.is_open[] = !m.is_open[] if m.is_open[] t = translation(menuscene)[] - y_for_top_align = height(menuscene.px_area[]) - listheight[] + y_for_top_align = height(menuscene.viewport[]) - listheight[] translate!(menuscene, t[1], y_for_top_align, t[3]) end return Consume(true) @@ -295,7 +295,7 @@ function initialize_block!(m::Menu; default = 1) t = translation(menuscene)[] # Hack to differentiate mousewheel and trackpad scrolling step = m.scroll_speed[] * y - new_y = max(min(t[2] - step, 0), height(menuscene.px_area[]) - listheight[]) + new_y = max(min(t[2] - step, 0), height(menuscene.viewport[]) - listheight[]) translate!(menuscene, t[1], new_y, t[3]) return Consume(true) else @@ -330,7 +330,7 @@ function initialize_block!(m::Menu; default = 1) end dropdown_arrow = scatter!( blockscene, symbol_pos; - marker=lift(iso -> iso ? '▴' : '▾', blockscene, m.is_open), + marker=lift(iso -> iso ? :utriangle : :dtriangle, blockscene, m.is_open), markersize = m.dropdown_arrow_size, color = m.dropdown_arrow_color, strokecolor = :transparent, diff --git a/src/makielayout/blocks/polaraxis.jl b/src/makielayout/blocks/polaraxis.jl index 805b473c596..dc2d7aa240d 100644 --- a/src/makielayout/blocks/polaraxis.jl +++ b/src/makielayout/blocks/polaraxis.jl @@ -2,9 +2,6 @@ ### Main Block Intialization ################################################################################ - -can_be_current_axis(ax::PolarAxis) = true - function initialize_block!(po::PolarAxis; palette=nothing) # Setup Scenes cb = po.layoutobservables.computedbbox @@ -22,14 +19,11 @@ function initialize_block!(po::PolarAxis; palette=nothing) transformation = Transformation(po.scene, transform_func = identity) ) - - # Setup Cycler - po.cycler = Cycler() - if palette === nothing - palette = fast_deepcopy(get(po.blockscene.theme, :palette, DEFAULT_PALETTES)) + if !isnothing(palette) + # Backwards compatibility for when palette was part of axis! + palette_attr = palette isa Attributes ? palette : Attributes(palette) + po.scene.theme.palette = palette_attr end - po.palette = palette isa Attributes ? palette : Attributes(palette) - # Setup camera/limits and Polar transform usable_fraction, radius_at_origin = setup_camera_matrices!(po) @@ -59,7 +53,7 @@ function initialize_block!(po::PolarAxis; palette=nothing) thetaticklabelplot.plots[1].fontsize, thetaticklabelplot.plots[1].font, po.thetaticklabelpad, - po.overlay.px_area + po.overlay.viewport ) do _, _, _, rpad, _, _, _, tpad, area # get maximum size of tick label @@ -90,7 +84,7 @@ function initialize_block!(po::PolarAxis; palette=nothing) po.target_rlims, po.target_thetalims, po.target_theta_0, po.direction, po.rticklabelsize, po.rticklabelpad, po.thetaticklabelsize, po.thetaticklabelpad, - po.overlay.px_area, po.overlay.camera.projectionview, + po.overlay.viewport, po.overlay.camera.projectionview, po.titlegap, po.titlesize, po.titlealign ) do rlims, thetalims, theta_0, dir, r_fs, r_pad, t_fs, t_pad, area, pv, gap, size, align @@ -258,14 +252,14 @@ function setup_camera_matrices!(po::PolarAxis) # update projection matrices # this just aspect-aware clip space (-1 .. 1, -h/w ... h/w, -max_z ... max_z) - on(po.blockscene, po.scene.px_area) do area + on(po.blockscene, po.scene.viewport) do area aspect = Float32((/)(widths(area)...)) w = 1f0 h = 1f0 / aspect camera(po.scene).projection[] = orthographicprojection(-w, w, -h, h, -max_z, max_z) end - on(po.blockscene, po.overlay.px_area) do area + on(po.blockscene, po.overlay.viewport) do area aspect = Float32((/)(widths(area)...)) w = 1f0 h = 1f0 / aspect @@ -386,7 +380,7 @@ function setup_camera_matrices!(po::PolarAxis) on(po.blockscene, e.mouseposition) do _ if drag_state[][3] - w = widths(po.scene.px_area[]) + w = widths(po.scene) p0 = (last_px_pos[] .- 0.5w) ./ w p1 = Point2f(mouseposition_px(po.scene) .- 0.5w) ./ w if norm(p0) * norm(p1) < 1e-6 @@ -769,7 +763,7 @@ function draw_axis!(po::PolarAxis, radius_at_origin) visible = po.clip, fxaa = false, transformation = Transformation(), # no polar transform for this - shading = false + shading = NoShading ) # inner clip is a (filled) circle sector which also needs to regenerate with @@ -791,7 +785,7 @@ function draw_axis!(po::PolarAxis, radius_at_origin) visible = po.clip, fxaa = false, transformation = Transformation(), - shading = false + shading = NoShading ) # handle placement with transform @@ -857,48 +851,13 @@ function draw_axis!(po::PolarAxis, radius_at_origin) return rticklabelplot, thetaticklabelplot end - -################################################################################ -### Plotting -################################################################################ - -# TODO: consider enabling this -# needs_tight_limits(::Surface) = true - -function plot!( - po::PolarAxis, P::PlotFunc, - attributes::Attributes, args...; - kw_attributes...) - - allattrs = merge(attributes, Attributes(kw_attributes)) - - cycle = get_cycle_for_plottype(allattrs, P) - add_cycle_attributes!(allattrs, P, cycle, po.cycler, po.palette) - - plot = plot!(po.scene, P, allattrs, args...) - - needs_tight_limits(plot) && tightlimits!(po) - - if is_open_or_any_parent(po.scene) - reset_limits!(po) - end - - plot -end - - -function plot!(P::PlotFunc, po::PolarAxis, args...; kw_attributes...) - attributes = Attributes(kw_attributes) - plot!(po, P, attributes, args...) -end - -delete!(ax::PolarAxis, p::AbstractPlot) = delete!(ax.scene, p) - function update_state_before_display!(ax::PolarAxis) reset_limits!(ax) return end +delete!(ax::PolarAxis, p::AbstractPlot) = delete!(ax.scene, p) + ################################################################################ ### Utilities ################################################################################ diff --git a/src/makielayout/blocks/scene.jl b/src/makielayout/blocks/scene.jl index 8ecbad040c6..e5cac177b0f 100644 --- a/src/makielayout/blocks/scene.jl +++ b/src/makielayout/blocks/scene.jl @@ -1,20 +1,9 @@ -function Makie.plot!( - lscene::LScene, P::Makie.PlotFunc, - attributes::Makie.Attributes, args...; - kw_attributes...) - - plot = Makie.plot!(lscene.scene, P, attributes, args...; kw_attributes...) +function reset_limits!(lscene::LScene) notify(lscene.scene.theme.limits) center!(lscene.scene) - plot -end - -function Makie.plot!(P::Makie.PlotFunc, ls::LScene, args...; kw_attributes...) - attributes = Makie.Attributes(kw_attributes) - _disallow_keyword(:axis, attributes) - _disallow_keyword(:figure, attributes) - Makie.plot!(ls, P, attributes, args...) + return end +tightlimits!(::LScene) = nothing # TODO implement!? function initialize_block!(ls::LScene; scenekw = NamedTuple()) blockscene = ls.blockscene @@ -61,8 +50,6 @@ function Base.delete!(ax::LScene, plot::AbstractPlot) ax end -can_be_current_axis(ls::LScene) = true - Makie.cam2d!(ax::LScene; kwargs...) = Makie.cam2d!(ax.scene; kwargs...) Makie.campixel!(ax::LScene; kwargs...) = Makie.campixel!(ax.scene; kwargs...) Makie.cam_relative!(ax::LScene; kwargs...) = Makie.cam_relative!(ax.scene; kwargs...) diff --git a/src/makielayout/blocks/textbox.jl b/src/makielayout/blocks/textbox.jl index 761ae53cd28..f679c7a9222 100644 --- a/src/makielayout/blocks/textbox.jl +++ b/src/makielayout/blocks/textbox.jl @@ -99,7 +99,7 @@ function initialize_block!(tbox::Textbox) end end - cursor = linesegments!(scene, cursorpoints, color = tbox.cursorcolor, linewidth = 2, inspectable = false) + cursor = linesegments!(scene, cursorpoints, color = tbox.cursorcolor, linewidth = 1, inspectable = false) tbox.cursoranimtask = nothing diff --git a/src/makielayout/helpers.jl b/src/makielayout/helpers.jl index 378d067f231..2732f804cc9 100644 --- a/src/makielayout/helpers.jl +++ b/src/makielayout/helpers.jl @@ -138,7 +138,7 @@ function tightlimits!(la::Axis, ::Top) end function GridLayoutBase.GridLayout(scene::Scene, args...; kwargs...) - return GridLayout(args...; bbox=lift(Rect2f, pixelarea(scene)), kwargs...) + return GridLayout(args...; bbox=lift(Rect2f, viewport(scene)), kwargs...) end function axislines!(scene, rect, spinewidth, topspinevisible, rightspinevisible, diff --git a/src/makielayout/interactions.jl b/src/makielayout/interactions.jl index 505b9d8c247..3945f0ba86e 100644 --- a/src/makielayout/interactions.jl +++ b/src/makielayout/interactions.jl @@ -123,7 +123,7 @@ end function _selection_vertices(ax_scene, outer, inner) _clamp(p, plow, phigh) = Point2f(clamp(p[1], plow[1], phigh[1]), clamp(p[2], plow[2], phigh[2])) - proj(point) = project(ax_scene, point) .+ minimum(ax_scene.px_area[]) + proj(point) = project(ax_scene, point) .+ minimum(ax_scene.viewport[]) transf = Makie.transform_func(ax_scene) outer = positivize(Makie.apply_transform(transf, outer)) inner = positivize(Makie.apply_transform(transf, inner)) @@ -242,7 +242,7 @@ function process_interaction(s::ScrollZoom, event::ScrollEvent, ax::Axis) cam = camera(scene) if zoom != 0 - pa = pixelarea(scene)[] + pa = viewport(scene)[] z = (1f0 - s.speed)^zoom @@ -305,7 +305,7 @@ function process_interaction(dp::DragPan, event::MouseEvent, ax) scene = ax.scene cam = camera(scene) - pa = pixelarea(scene)[] + pa = viewport(scene)[] mp_axscene = Vec4f((event.px .- pa.origin)..., 0, 1) mp_axscene_prev = Vec4f((event.prev_px .- pa.origin)..., 0, 1) diff --git a/src/makielayout/lineaxis.jl b/src/makielayout/lineaxis.jl index 7446dbc1eea..6e9a416f34e 100644 --- a/src/makielayout/lineaxis.jl +++ b/src/makielayout/lineaxis.jl @@ -1,3 +1,8 @@ +# the hyphen which is usually used to store negative number strings +# is shorter than the dedicated minus in most fonts, the minus glyph +# looks more balanced with numbers, especially in superscripts or subscripts +const MINUS_SIGN = "−" + function LineAxis(parent::Scene; @nospecialize(kwargs...)) attrs = merge!(Attributes(kwargs), generic_plot_attributes(LineAxis)) return LineAxis(parent, attrs) @@ -600,7 +605,7 @@ function get_ticks(l::LogTicks, scale::LogFunctions, ::Automatic, vmin, vmax) xs -> Showoff.showoff(xs, :plain), ticks_scaled ) - labels = rich.(_logbase(scale), superscript.(labels_scaled, offset = Vec2f(0.1f0, 0f0))) + labels = rich.(_logbase(scale), superscript.(replace.(labels_scaled, "-" => MINUS_SIGN), offset = Vec2f(0.1f0, 0f0))) ticks, labels end @@ -663,9 +668,9 @@ end """ get_ticklabels(::Automatic, values) -Gets tick labels by applying `showoff` to `values`. +Gets tick labels by applying `showoff_minus` to `values`. """ -get_ticklabels(::Automatic, values) = Showoff.showoff(values) +get_ticklabels(::Automatic, values) = showoff_minus(values) """ get_ticklabels(formatfunction::Function, values) @@ -686,7 +691,7 @@ function get_ticks(m::MultiplesTicks, any_scale, ::Automatic, vmin, vmax) dvmax = vmax / m.multiple multiples = Makie.get_tickvalues(LinearTicks(m.n_ideal), dvmin, dvmax) - multiples .* m.multiple, Showoff.showoff(multiples) .* m.suffix + multiples .* m.multiple, showoff_minus(multiples) .* m.suffix end function get_ticks(m::AngularTicks, any_scale, ::Automatic, vmin, vmax) @@ -718,7 +723,13 @@ function get_ticks(m::AngularTicks, any_scale, ::Automatic, vmin, vmax) # We also need to be careful that we don't remove significant digits sigdigits = ceil(Int, log10(1000 * max(abs(vmin), abs(vmax)) / delta)) - return multiples, Showoff.showoff(round.(multiples .* m.label_factor, sigdigits = sigdigits)) .* m.suffix + return multiples, showoff_minus(round.(multiples .* m.label_factor, sigdigits = sigdigits)) .* m.suffix +end + +# Replaces hyphens in negative numbers with the unicode MINUS_SIGN +function showoff_minus(x::AbstractVector) + # TODO: don't use the `replace` workaround + replace.(Showoff.showoff(x), r"-(?=\d)" => MINUS_SIGN) end # identity or unsupported scales diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index 90fc9e6526d..f9a71ebd212 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -8,14 +8,6 @@ end struct DataAspect end - -struct Cycler - counters::IdDict{Type, Int} -end - -Cycler() = Cycler(IdDict{Type, Int}()) - - struct Cycle cycle::Vector{Pair{Vector{Symbol}, Symbol}} covary::Bool @@ -205,14 +197,12 @@ struct KeysEvent keys::Set{Makie.Keyboard.Button} end -@Block Axis begin +@Block Axis <: AbstractAxis begin scene::Scene xaxislinks::Vector{Axis} yaxislinks::Vector{Axis} targetlimits::Observable{Rect2f} finallimits::Observable{Rect2f} - cycler::Cycler - palette::Attributes block_limit_linking::Observable{Bool} mouseeventhandle::MouseEventHandle scrollevents::Observable{ScrollEvent} @@ -330,9 +320,9 @@ end "The horizontal and vertical alignment of the yticklabels." yticklabelalign::Union{Makie.Automatic, Tuple{Symbol, Symbol}} = Makie.automatic "The size of the xtick marks." - xticksize::Float64 = 6f0 + xticksize::Float64 = 5f0 "The size of the ytick marks." - yticksize::Float64 = 6f0 + yticksize::Float64 = 5f0 "Controls if the xtick marks are visible." xticksvisible::Bool = true "Controls if the ytick marks are visible." @@ -589,7 +579,7 @@ end "The alignment of x minor ticks on the axis spine" xminortickalign::Float64 = 0f0 "The tick size of x minor ticks" - xminorticksize::Float64 = 4f0 + xminorticksize::Float64 = 3f0 "The tick width of x minor ticks" xminortickwidth::Float64 = 1f0 "The tick color of x minor ticks" @@ -608,7 +598,7 @@ end "The alignment of y minor ticks on the axis spine" yminortickalign::Float64 = 0f0 "The tick size of y minor ticks" - yminorticksize::Float64 = 4f0 + yminorticksize::Float64 = 3f0 "The tick width of y minor ticks" yminortickwidth::Float64 = 1f0 "The tick color of y minor ticks" @@ -671,7 +661,7 @@ function RectangleZoom(f::Function, ax::Axis; kw...) faces = [1 2 5; 5 2 6; 2 3 6; 6 3 7; 3 4 7; 7 4 8; 4 1 8; 8 1 5] # plot to blockscene, so ax.scene stays exclusive for user plots # That's also why we need to pass `ax.scene` to _selection_vertices, so it can project to that space - mesh = mesh!(ax.blockscene, selection_vertices, faces, color = (:black, 0.2), shading = false, + mesh = mesh!(ax.blockscene, selection_vertices, faces, color = (:black, 0.2), shading = NoShading, inspectable = false, visible=r.active, transparency=true) # translate forward so selection mesh and frame are never behind data translate!(mesh, 0, 0, 100) @@ -713,7 +703,7 @@ end "The color of the tick labels." ticklabelcolor = @inherit(:textcolor, :black) "The size of the tick marks." - ticksize = 6f0 + ticksize = 5f0 "Controls if the tick marks are visible." ticksvisible = true "The ticks." @@ -795,7 +785,7 @@ end "The alignment of minor ticks on the axis spine" minortickalign = 0f0 "The tick size of minor ticks" - minorticksize = 4f0 + minorticksize = 3f0 "The tick width of minor ticks" minortickwidth = 1f0 "The tick color of minor ticks" @@ -803,7 +793,7 @@ end "The tick locator for the minor ticks" minorticks = IntervalsBetween(5) "The width or height of the colorbar, depending on if it's vertical or horizontal, unless overridden by `width` / `height`" - size = 16 + size = 12 end end @@ -901,7 +891,7 @@ end "The current value of the slider. Don't set this manually, use the function `set_close_to!`." value = 0 "The width of the slider line" - linewidth::Float32 = 15 + linewidth::Float32 = 10 "The color of the slider when the mouse hovers over it." color_active_dimmed::RGBAf = COLOR_ACCENT_DIMMED[] "The color of the slider when the mouse clicks and drags the slider." @@ -965,7 +955,7 @@ end "The current interval of the slider. Don't set this manually, use the function `set_close_to!`." interval = (0, 0) "The width of the slider line" - linewidth::Float64 = 15.0 + linewidth::Float64 = 10.0 "The color of the slider when the mouse hovers over it." color_active_dimmed::RGBAf = COLOR_ACCENT_DIMMED[] "The color of the slider when the mouse clicks and drags the slider." @@ -988,7 +978,7 @@ end "The vertical alignment of the button in its suggested boundingbox" valign = :center "The extra space added to the sides of the button label's boundingbox." - padding = (10f0, 10f0, 10f0, 10f0) + padding = (8f0, 8f0, 8f0, 8f0) "The font size of the button label." fontsize = @inherit(:fontsize, 16f0) "The text of the button label." @@ -1037,9 +1027,9 @@ end "The vertical alignment of the toggle in its suggested bounding box." valign = :center "The width of the toggle." - width = 60 + width = 32 "The height of the toggle." - height = 28 + height = 18 "Controls if the parent layout can adjust to this element's width" tellwidth = true "Controls if the parent layout can adjust to this element's height" @@ -1101,13 +1091,13 @@ end "Color of the dropdown arrow" dropdown_arrow_color = (:black, 0.2) "Size of the dropdown arrow" - dropdown_arrow_size = 20 + dropdown_arrow_size = 10 "The list of options selectable in the menu. This can be any iterable of a mixture of strings and containers with one string and one other value. If an entry is just a string, that string is both label and selection. If an entry is a container with one string and one other value, the string is the label and the other value is the selection." options = ["no options"] "Font size of the cell texts" fontsize = @inherit(:fontsize, 16f0) "Padding of entry texts" - textpadding = (10, 10, 10, 10) + textpadding = (8, 10, 8, 8) "Color of entry texts" textcolor = :black "The opening direction of the menu (:up or :down)" @@ -1186,7 +1176,7 @@ const EntryGroup = Tuple{Any, Vector{LegendEntry}} "The vertical alignment of the entry labels." labelvalign = :center "The additional space between the legend content and the border." - padding = (10f0, 10f0, 8f0, 8f0) + padding = (6f0, 6f0, 6f0, 6f0) "The additional space between the legend and its suggested boundingbox." margin = (0f0, 0f0, 0f0, 0f0) "The background color of the legend. DEPRECATED - use `backgroundcolor` instead." @@ -1260,7 +1250,7 @@ const EntryGroup = Tuple{Any, Vector{LegendEntry}} end end -@Block LScene begin +@Block LScene <: AbstractAxis begin scene::Scene @attributes begin "The height setting of the scene." @@ -1335,13 +1325,13 @@ end "Color of the box border when focused and invalid." bordercolor_focused_invalid = RGBf(1, 0, 0) "Width of the box border." - borderwidth = 2f0 + borderwidth = 1f0 "Padding of the text against the box." - textpadding = (10, 10, 10, 10) + textpadding = (8, 8, 8, 8) "If the textbox is focused and receives text input." focused = false "Corner radius of text box." - cornerradius = 8 + cornerradius = 5 "Corner segments of one rounded corner." cornersegments = 20 "Validator that is called with validate_textbox(string, validator) to determine if the current string is valid. Can by default be a RegEx that needs to match the complete string, or a function taking a string as input and returning a Bool. If the validator is a type T (for example Float64), validation will be `tryparse(string, T)`." @@ -1353,15 +1343,13 @@ end end end -@Block Axis3 begin +@Block Axis3 <: AbstractAxis begin scene::Scene finallimits::Observable{Rect3f} mouseeventhandle::MouseEventHandle scrollevents::Observable{ScrollEvent} keysevents::Observable{KeysEvent} interactions::Dict{Symbol, Tuple{Bool, Any}} - cycler::Cycler - palette::Attributes @attributes begin "The height setting of the scene." height = nothing @@ -1642,14 +1630,12 @@ end end end -@Block PolarAxis begin +@Block PolarAxis <: AbstractAxis begin scene::Scene overlay::Scene target_rlims::Observable{Tuple{Float64, Float64}} target_thetalims::Observable{Tuple{Float64, Float64}} target_theta_0::Observable{Float32} - cycler::Cycler - palette::Attributes @attributes begin # Generic diff --git a/src/precompiles.jl b/src/precompiles.jl index 7b71d31148d..1ed6f3e0054 100644 --- a/src/precompiles.jl +++ b/src/precompiles.jl @@ -44,3 +44,11 @@ for T in (DragPan, RectangleZoom, LimitReset) end precompile(process_axis_event, (Axis, MouseEvent)) precompile(process_interaction, (ScrollZoom, ScrollEvent, Axis)) +precompile(el32convert, (Vector{Int64},)) +precompile(translate, (MoveTo, Vec2{Float64})) +precompile(scale, (MoveTo, Vec{2,Float32})) +precompile(append!, (Vector{FreeType.FT_Vector_}, Vector{FreeType.FT_Vector_})) +precompile(convert_command, (MoveTo,)) +precompile(plot!, (MakieCore.Text{Tuple{Vector{Point{2, Float32}}}},)) +precompile(Vec2{Float64}, (Tuple{Int64,Int64},)) +precompile(MakieCore._create_plot, (typeof(scatter), Dict{Symbol,Any}, UnitRange{Int64})) diff --git a/src/recording.jl b/src/recording.jl index 18cc72b0d6f..a06999a8251 100644 --- a/src/recording.jl +++ b/src/recording.jl @@ -27,13 +27,19 @@ mutable struct RamStepper end function Stepper(figlike::FigureLike; backend=current_backend(), format=:png, visible=false, connect=false, screen_kw...) - screen = getscreen(backend, get_scene(figlike), JuliaNative; visible=visible, start_renderloop=false, screen_kw...) + config = Dict{Symbol,Any}(screen_kw) + get!(config, :visible, visible) + get!(config, :start_renderloop, false) + screen = getscreen(backend, get_scene(figlike), config, JuliaNative) display(screen, figlike; connect=connect) return RamStepper(figlike, screen, Matrix{RGBf}[], format) end function Stepper(figlike::FigureLike, path::String, step::Int; format=:png, backend=current_backend(), visible=false, connect=false, screen_kw...) - screen = getscreen(backend, get_scene(figlike), JuliaNative; visible=visible, start_renderloop=false, screen_kw...) + config = Dict{Symbol,Any}(screen_kw) + get!(config, :visible, visible) + get!(config, :start_renderloop, false) + screen = getscreen(backend, get_scene(figlike), config, JuliaNative) display(screen, figlike; connect=connect) return FolderStepper(figlike, screen, path, format, step) end diff --git a/src/scenes.jl b/src/scenes.jl index 319286b993d..6802d2ea652 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -37,33 +37,6 @@ function SSAO(; radius=nothing, bias=nothing, blur=nothing) return SSAO(_radius, _bias, _blur) end -abstract type AbstractLight end - -""" -A positional point light, shining at a certain color. -Color values can be bigger than 1 for brighter lights. -""" -struct PointLight <: AbstractLight - position::Observable{Vec3f} - radiance::Observable{RGBf} -end - -""" -An environment Light, that uses a spherical environment map to provide lighting. -See: https://en.wikipedia.org/wiki/Reflection_mapping -""" -struct EnvironmentLight <: AbstractLight - intensity::Observable{Float32} - image::Observable{Matrix{RGBf}} -end - -""" -A simple, one color ambient light. -""" -struct AmbientLight <: AbstractLight - color::Observable{RGBf} -end - """ Scene TODO document this @@ -81,7 +54,7 @@ mutable struct Scene <: AbstractScene events::Events "The current pixel area of the Scene." - px_area::Observable{Rect2i} + viewport::Observable{Rect2i} "Whether the scene should be cleared." clear::Observable{Bool} @@ -114,11 +87,12 @@ mutable struct Scene <: AbstractScene ssao::SSAO lights::Vector{AbstractLight} deregister_callbacks::Vector{Observables.ObserverFunction} + cycler::Cycler function Scene( parent::Union{Nothing, Scene}, events::Events, - px_area::Observable{Rect2i}, + viewport::Observable{Rect2i}, clear::Observable{Bool}, camera::Camera, camera_controls::AbstractCamera, @@ -130,12 +104,12 @@ mutable struct Scene <: AbstractScene backgroundcolor::Observable{RGBAf}, visible::Observable{Bool}, ssao::SSAO, - lights::Vector{AbstractLight} + lights::Vector ) scene = new( parent, events, - px_area, + viewport, clear, camera, camera_controls, @@ -147,8 +121,9 @@ mutable struct Scene <: AbstractScene backgroundcolor, visible, ssao, - lights, - Observables.ObserverFunction[] + convert(Vector{AbstractLight}, lights), + Observables.ObserverFunction[], + Cycler() ) finalizer(free, scene) return scene @@ -156,19 +131,19 @@ mutable struct Scene <: AbstractScene end # on & map versions that deregister when scene closes! -function Observables.on(f, scene::Union{Combined,Scene}, observable::Observable; update=false, priority=0) - to_deregister = on(f, observable; update=update, priority=priority) - push!(scene.deregister_callbacks, to_deregister) +function Observables.on(@nospecialize(f), @nospecialize(scene::Union{Plot,Scene}), @nospecialize(observable::Observable); update=false, priority=0) + to_deregister = on(f, observable; update=update, priority=priority)::Observables.ObserverFunction + push!(scene.deregister_callbacks::Vector{Observables.ObserverFunction}, to_deregister) return to_deregister end -function Observables.onany(f, scene::Union{Combined,Scene}, observables...; priority=0) +function Observables.onany(@nospecialize(f), @nospecialize(scene::Union{Plot,Scene}), @nospecialize(observables...); priority=0) to_deregister = onany(f, observables...; priority=priority) - append!(scene.deregister_callbacks, to_deregister) + append!(scene.deregister_callbacks::Vector{Observables.ObserverFunction}, to_deregister) return to_deregister end -@inline function Base.map!(@nospecialize(f), scene::Union{Combined,Scene}, result::AbstractObservable, os...; +@inline function Base.map!(f, @nospecialize(scene::Union{Plot,Scene}), result::AbstractObservable, os...; update::Bool=true, priority = 0) # note: the @inline prevents de-specialization due to the splatting callback = Observables.MapCallback(f, result, os) @@ -179,7 +154,7 @@ end return result end -@inline function Base.map(f::F, scene::Union{Combined,Scene}, arg1::AbstractObservable, args...; +@inline function Base.map(f::F, @nospecialize(scene::Union{Plot,Scene}), arg1::AbstractObservable, args...; ignore_equal_values=false, priority = 0) where {F} # note: the @inline prevents de-specialization due to the splatting obs = Observable(f(arg1[], map(Observables.to_value, args)...); ignore_equal_values=ignore_equal_values) @@ -216,7 +191,7 @@ function Base.show(io::IO, scene::Scene) end function Scene(; - px_area::Union{Observable{Rect2i}, Nothing} = nothing, + viewport::Union{Observable{Rect2i}, Nothing} = nothing, events::Events = Events(), clear::Union{Automatic, Observable{Bool}, Bool} = automatic, transform_func=identity, @@ -239,12 +214,18 @@ function Scene(; bg = Observable{RGBAf}(to_color(m_theme.backgroundcolor[]); ignore_equal_values=true) - wasnothing = isnothing(px_area) + wasnothing = isnothing(viewport) if wasnothing - px_area = Observable(Recti(0, 0, m_theme.resolution[]); ignore_equal_values=true) + sz = if haskey(m_theme, :resolution) + @warn "Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions." + m_theme.resolution[] + else + m_theme.size[] + end + viewport = Observable(Recti(0, 0, sz); ignore_equal_values=true) end - cam = camera isa Camera ? camera : Camera(px_area) + cam = camera isa Camera ? camera : Camera(viewport) _lights = lights isa Automatic ? AbstractLight[] : lights # if we have an opaque background, automatically set clear to true! @@ -254,7 +235,7 @@ function Scene(; clear = convert(Observable{Bool}, clear) end scene = Scene( - parent, events, px_area, clear, cam, camera_controls, + parent, events, viewport, clear, cam, camera_controls, transformation, plots, m_theme, children, current_screens, bg, visible, ssao, _lights ) @@ -262,51 +243,46 @@ function Scene(; if wasnothing on(events.window_area, priority = typemax(Int)) do w_area - if !any(x -> x ≈ 0.0, widths(w_area)) && px_area[] != w_area - px_area[] = w_area + if !any(x -> x ≈ 0.0, widths(w_area)) && viewport[] != w_area + viewport[] = w_area end return Consume(false) end end if lights isa Automatic - lightposition = to_value(get(m_theme, :lightposition, nothing)) - if !isnothing(lightposition) - position = if lightposition === :eyeposition - scene.camera.eyeposition - elseif lightposition isa Vec3 - m_theme.lightposition - else - error("Wrong lightposition type, use `:eyeposition` or `Vec3f(...)`") + haskey(m_theme, :lightposition) && @warn("`lightposition` is deprecated. Set `light_direction` instead.") + + if haskey(m_theme, :lights) + copyto!(scene.lights, m_theme.lights[]) + else + haskey(m_theme, :light_direction) || error("Theme must contain `light_direction::Vec3f` or an explicit `lights::Vector`!") + haskey(m_theme, :light_color) || error("Theme must contain `light_color::RGBf` or an explicit `lights::Vector`!") + haskey(m_theme, :camera_relative_light) || @warn("Theme should contain `camera_relative_light::Bool`.") + + if haskey(m_theme, :ambient) + push!(scene.lights, AmbientLight(m_theme[:ambient][])) end - push!(scene.lights, PointLight(position, RGBf(1, 1, 1))) - end - ambient = to_value(get(m_theme, :ambient, nothing)) - if !isnothing(ambient) - push!(scene.lights, AmbientLight(ambient)) + + push!(scene.lights, DirectionalLight( + m_theme[:light_color][], m_theme[:light_direction], + to_value(get(m_theme, :camera_relative_light, false)) + )) end end return scene end -function get_one_light(scene::Scene, Typ) - indices = findall(x-> x isa Typ, scene.lights) - isempty(indices) && return nothing - if length(indices) > 1 - @warn("Only one light supported by backend right now. Using only first light") - end - return scene.lights[indices[1]] -end - -get_point_light(scene::Scene) = get_one_light(scene, PointLight) -get_ambient_light(scene::Scene) = get_one_light(scene, AmbientLight) - +get_directional_light(scene::Scene) = get_one_light(scene.lights, DirectionalLight) +get_point_light(scene::Scene) = get_one_light(scene.lights, PointLight) +get_ambient_light(scene::Scene) = get_one_light(scene.lights, AmbientLight) +default_shading!(plot, scene::Scene) = default_shading!(plot, scene.lights) function Scene( parent::Scene; events=parent.events, - px_area=nothing, + viewport=nothing, clear=false, camera=nothing, camera_controls=parent.camera_controls, @@ -317,10 +293,10 @@ function Scene( if camera !== parent.camera camera_controls = EmptyCamera() end - child_px_area = px_area isa Observable ? px_area : Observable(Rect2i(0, 0, 0, 0); ignore_equal_values=true) + child_px_area = viewport isa Observable ? viewport : Observable(Rect2i(0, 0, 0, 0); ignore_equal_values=true) child = Scene(; events=events, - px_area=child_px_area, + viewport=child_px_area, clear=convert(Observable{Bool}, clear), camera=camera, camera_controls=camera_controls, @@ -330,13 +306,13 @@ function Scene( theme=theme(parent), kw... ) - if isnothing(px_area) - map!(identity, child, child_px_area, parent.px_area) - elseif px_area isa Rect2 - child_px_area[] = Rect2i(px_area) + if isnothing(viewport) + map!(identity, child, child_px_area, parent.viewport) + elseif viewport isa Rect2 + child_px_area[] = Rect2i(viewport) else - if !(px_area isa Observable) - error("px_area must be an Observable{Rect2} or a Rect2") + if !(viewport isa Observable) + error("viewport must be an Observable{Rect2} or a Rect2") end end push!(parent.children, child) @@ -346,7 +322,7 @@ end # legacy constructor function Scene(parent::Scene, area; kw...) - return Scene(parent; px_area=area, kw...) + return Scene(parent; viewport=area, kw...) end # Base overloads for Scene @@ -363,16 +339,17 @@ function root(scene::Scene) end parent_or_self(scene::Scene) = isroot(scene) ? scene : parent(scene) -GeometryBasics.widths(scene::Scene) = widths(to_value(pixelarea(scene))) +GeometryBasics.widths(scene::Scene) = widths(to_value(viewport(scene))) Base.size(scene::Scene) = Tuple(widths(scene)) Base.size(x::Scene, i) = size(x)[i] + function Base.resize!(scene::Scene, xy::Tuple{Number,Number}) resize!(scene, Recti(0, 0, xy)) end Base.resize!(scene::Scene, x::Number, y::Number) = resize!(scene, (x, y)) function Base.resize!(scene::Scene, rect::Rect2) - pixelarea(scene)[] = rect + viewport(scene)[] = rect if isroot(scene) for screen in scene.current_screens resize!(screen, widths(rect)...) @@ -423,7 +400,7 @@ end function free(scene::Scene) empty!(scene; free=true) - for field in [:backgroundcolor, :px_area, :visible] + for field in [:backgroundcolor, :viewport, :visible] Observables.clear(getfield(scene, field)) end for screen in copy(scene.current_screens) @@ -465,20 +442,22 @@ function Base.empty!(scene::Scene; free=false) return nothing end +function Base.push!(plot::Plot, subplot) + subplot.parent = plot + push!(plot.plots, subplot) +end -Base.push!(scene::Combined, subscene) = nothing # Combined plots add themselves uppon creation - -function Base.push!(scene::Scene, plot::AbstractPlot) +function Base.push!(scene::Scene, @nospecialize(plot::AbstractPlot)) push!(scene.plots, plot) - plot isa Combined || (plot.parent[] = scene) for screen in scene.current_screens - insert!(screen, scene, plot) + Base.invokelatest(insert!, screen, scene, plot) end end function Base.delete!(screen::MakieScreen, ::Scene, ::AbstractPlot) - @warn "Deleting plots not implemented for backend: $(typeof(screen))" + @debug "Deleting plots not implemented for backend: $(typeof(screen))" end + function Base.delete!(screen::MakieScreen, ::Scene) # This may not be necessary for every backed @debug "Deleting scenes not implemented for backend: $(typeof(screen))" @@ -488,6 +467,9 @@ function free(plot::AbstractPlot) for f in plot.deregister_callbacks Observables.off(f) end + for arg in plot.args + Observables.clear(arg) + end foreach(free, plot.plots) empty!(plot.plots) empty!(plot.deregister_callbacks) @@ -508,21 +490,6 @@ function Base.delete!(scene::Scene, plot::AbstractPlot) free(plot) end -function Base.push!(scene::Scene, child::Scene) - push!(scene.children, child) - disconnect!(child.camera) - observables = map([:view, :projection, :projectionview, :resolution, :eyeposition]) do field - return lift(getfield(scene.camera, field)) do val - getfield(child.camera, field)[] = val - getfield(child.camera, field)[] = val - return - end - end - cameracontrols!(child, observables) - child.parent = scene - return scene -end - events(x) = events(get_scene(x)) events(scene::Scene) = scene.events events(scene::SceneLike) = events(scene.parent) @@ -542,9 +509,14 @@ end cameracontrols!(scene::SceneLike, cam) = cameracontrols!(parent(scene), cam) cameracontrols!(x, cam) = cameracontrols!(get_scene(x), cam) -pixelarea(x) = pixelarea(get_scene(x)) -pixelarea(scene::Scene) = scene.px_area -pixelarea(scene::SceneLike) = pixelarea(scene.parent) +viewport(x) = viewport(get_scene(x)) +""" + viewport(scene::Scene) + +Gets the viewport of the scene in device independent units as an `Observable{Rect2{Int}}`. +""" +viewport(scene::Scene) = scene.viewport +viewport(scene::SceneLike) = viewport(scene.parent) plots(x) = plots(get_scene(x)) plots(scene::SceneLike) = scene.plots @@ -562,7 +534,7 @@ function plots_from_camera(scene::Scene, camera::Camera, list=AbstractPlot[]) end -function insertplots!(screen::AbstractDisplay, scene::Scene) +function insertplots!(@nospecialize(screen::AbstractDisplay), scene::Scene) for elem in scene.plots insert!(screen, scene, elem) end @@ -587,7 +559,7 @@ function center!(scene::Scene, padding=0.01, exclude = not_in_data_space) end parent_scene(x) = parent_scene(get_scene(x)) -parent_scene(x::Combined) = parent_scene(parent(x)) +parent_scene(x::Plot) = parent_scene(parent(x)) parent_scene(x::Scene) = x Base.isopen(x::SceneLike) = events(x).window_open[] @@ -627,22 +599,22 @@ end const FigureLike = Union{Scene, Figure, FigureAxisPlot} """ - is_atomic_plot(plot::Combined) + is_atomic_plot(plot::Plot) Defines what Makie considers an atomic plot, used in `collect_atomic_plots`. Backends may have a different definition of what is considered an atomic plot, but instead of overloading this function, they should create their own definition and pass it to `collect_atomic_plots` """ -is_atomic_plot(plot::Combined) = isempty(plot.plots) +is_atomic_plot(plot::Plot) = isempty(plot.plots) """ collect_atomic_plots(scene::Scene, plots = AbstractPlot[]; is_atomic_plot = is_atomic_plot) - collect_atomic_plots(x::Combined, plots = AbstractPlot[]; is_atomic_plot = is_atomic_plot) + collect_atomic_plots(x::Plot, plots = AbstractPlot[]; is_atomic_plot = is_atomic_plot) Collects all plots in the provided `<: ScenePlot` and returns a vector of all plots which satisfy `is_atomic_plot`, which defaults to Makie's definition of `Makie.is_atomic_plot`. """ -function collect_atomic_plots(xplot::Combined, plots=AbstractPlot[]; is_atomic_plot=is_atomic_plot) +function collect_atomic_plots(xplot::Plot, plots=AbstractPlot[]; is_atomic_plot=is_atomic_plot) if is_atomic_plot(xplot) # Atomic plot! push!(plots, xplot) @@ -666,5 +638,3 @@ function collect_atomic_plots(scene::Scene, plots=AbstractPlot[]; is_atomic_plot collect_atomic_plots(scene.children, plots; is_atomic_plot=is_atomic_plot) plots end - -Base.@deprecate flatten_plots(scenelike) collect_atomic_plots(scenelike) diff --git a/src/specapi.jl b/src/specapi.jl new file mode 100644 index 00000000000..d9d5c13bebd --- /dev/null +++ b/src/specapi.jl @@ -0,0 +1,742 @@ + +using GridLayoutBase: GridLayoutBase + +import GridLayoutBase: GridPosition, Side, ContentSize, GapSize, AlignMode, Inner, GridLayout, GridSubposition + + +function get_recipe_function(name::Symbol) + if hasproperty(Makie, name) + return getfield(Makie, name) + else + return nothing + end +end + +@nospecialize +""" + PlotSpec(plottype, args...; kwargs...) + +Object encoding positional arguments (`args`), a `NamedTuple` of attributes (`kwargs`) +as well as plot type `P` of a basic plot. +""" +struct PlotSpec + type::Symbol + args::Vector{Any} + kwargs::Dict{Symbol, Any} + function PlotSpec(type::Symbol, args...; kwargs...) + type_str = string(type) + if type_str[end] == '!' + error("PlotSpec objects are supposed to be used without !, unless when using `S.$(type)(axis::P.Axis, args...; kwargs...)`") + end + if !isuppercase(type_str[1]) + func = get_recipe_function(type) + func === nothing && error("PlotSpec need to be existing recipes or Makie plot objects. Found: $(type_str)") + plot_type = Plot{func} + type = plotsym(plot_type) + @warn("PlotSpec objects are supposed to be title case. Found: $(type_str). Please use $(type) instead.") + end + kw = Dict{Symbol,Any}() + for (k, v) in kwargs + # convert eagerly, so that we have stable types for matching later + # E.g. so that PlotSpec(; color = :red) has the same type as PlotSpec(; color = RGBA(1, 0, 0, 1)) + if v isa Cycled # special case for conversions needing a scene + kw[k] = v + elseif v isa Observable + error("PlotSpec are supposed to be used without Observables") + else + try + # Really unfortunate! + # Recipes don't have convert_attribute + # (e.g. band(...; color=:y)) + # So on error we don't convert for now via try catch + # Since we also dont have an API to figure out if a convert is defined correctly + # TODO, I think we can do this more elegantly but will need a bit of a convert_attribute refactor + kw[k] = convert_attribute(v, Key{k}(), Key{type}()) + catch e + kw[k] = v + end + end + end + return new(type, Any[args...], kw) + end + PlotSpec(args...; kwargs...) = new(:plot, args...; kwargs...) +end +@specialize + +Base.getindex(p::PlotSpec, i::Int) = getindex(p.args, i) +Base.getindex(p::PlotSpec, i::Symbol) = getproperty(p.kwargs, i) + +to_plotspec(::Type{P}, args; kwargs...) where {P} = PlotSpec(plotsym(P), args...; kwargs...) + +function to_plotspec(::Type{P}, p::PlotSpec; kwargs...) where {P} + S = plottype(p) + return PlotSpec(plotsym(plottype(P, S)), p.args...; p.kwargs..., kwargs...) +end + +plottype(p::PlotSpec) = getfield(Makie, p.type) + +struct BlockSpec + type::Symbol # Type as :Scatter, :BarPlot + kwargs::Dict{Symbol,Any} + plots::Vector{PlotSpec} +end + +function BlockSpec(typ::Symbol, args...; plots::Vector{PlotSpec}=PlotSpec[], kw...) + attr = Dict{Symbol,Any}(kw) + if typ == :Legend + # TODO, this is hacky and works around the fact, + # that legend gets its legend elements from the positional arguments + # But we can only update them via legend.entrygroups + defaults = block_defaults(:Legend, attr, nothing) + entrygroups = to_entry_group(Attributes(defaults), args...) + attr[:entrygroups] = entrygroups + return BlockSpec(typ, attr, plots) + else + if !isempty(args) + error("BlockSpecs, with an exception for Legend, don't support positional arguments yet.") + end + return BlockSpec(typ, attr, plots) + end +end + +const GridLayoutPosition = Tuple{UnitRange{Int},UnitRange{Int},Side} + +to_span(range::UnitRange{Int}, span::UnitRange{Int}) = (range.start < span.start || range.stop > span.stop) ? error("Range $range not completely covered by spanning range $span.") : range +to_span(range::Int, span::UnitRange{Int}) = (range < span.start || range > span.stop) ? error("Range $range not completely covered by spanning range $span.") : range:range +to_span(::Colon, span::UnitRange{Int}) = span +to_gridposition(rows_cols::Tuple{Any,Any}, rowspan, colspan) = to_gridposition((rows_cols..., Inner()), rowspan, colspan) +to_gridposition(rows_cols_side::Tuple{Any,Any,Any}, rowspan, colspan) = (to_span(rows_cols_side[1], rowspan), to_span(rows_cols_side[2], colspan), rows_cols_side[3]) + +rangeunion(r1, r2::UnitRange) = min(r1.start, r2.start):max(r1.stop, r2.stop) +rangeunion(r1, r2::Int) = min(r1.start, r2):max(r1.stop, r2) +rangeunion(r1, r2::Colon) = r1 + +struct GridLayoutSpec + content::Vector{Pair{GridLayoutPosition,Union{GridLayoutSpec,BlockSpec}}} + + size::Tuple{Int, Int} + offsets::Tuple{Int, Int} + + colsizes::Vector{ContentSize} + rowsizes::Vector{ContentSize} + colgaps::Vector{GapSize} + rowgaps::Vector{GapSize} + alignmode::AlignMode + tellheight::Bool + tellwidth::Bool + halign::Float64 + valign::Float64 + + function GridLayoutSpec( + content::AbstractVector{<:Pair}; + colsizes = nothing, + rowsizes = nothing, + colgaps = nothing, + rowgaps = nothing, + alignmode::AlignMode = GridLayoutBase.Inside(), + tellheight::Bool = true, + tellwidth::Bool = true, + halign::Union{Symbol,Real} = :center, + valign::Union{Symbol,Real} = :center, + ) + + rowspan, colspan = foldl(content; init = (1:1, 1:1)) do (rows, cols), ((_rows, _cols, _...), _) + rangeunion(rows, _rows), rangeunion(cols, _cols) + end + + content = map(content) do (position, x) + p = Pair{GridLayoutPosition,Union{GridLayoutSpec,BlockSpec}}(to_gridposition(position, rowspan, colspan), x) + return p + end + + nrows = length(rowspan) + ncols = length(colspan) + colsizes = GridLayoutBase.convert_contentsizes(ncols, colsizes) + rowsizes = GridLayoutBase.convert_contentsizes(nrows, rowsizes) + default_rowgap = Fixed(16) # TODO: where does this come from? + default_colgap = Fixed(16) # TODO: where does this come from? + colgaps = GridLayoutBase.convert_gapsizes(ncols - 1, colgaps, default_colgap) + rowgaps = GridLayoutBase.convert_gapsizes(nrows - 1, rowgaps, default_rowgap) + + halign = GridLayoutBase.halign2shift(halign) + valign = GridLayoutBase.valign2shift(valign) + + return new( + content, + (nrows, ncols), + (rowspan[1] - 1, colspan[1] - 1), + colsizes, + rowsizes, + colgaps, + rowgaps, + alignmode, + tellheight, + tellwidth, + halign, + valign, + ) + end +end + +const Layoutable = Union{GridLayout,Block} +const LayoutableSpec = Union{GridLayoutSpec,BlockSpec} +const LayoutEntry = Pair{GridLayoutPosition,LayoutableSpec} + +GridLayoutSpec(v::AbstractVector; kwargs...) = GridLayoutSpec(reshape(v, :, 1); kwargs...) +function GridLayoutSpec(v::AbstractMatrix; kwargs...) + indices = vec([Tuple(c) for c in CartesianIndices(v)]) + pairs = [ + LayoutEntry((i:i, j:j, GridLayoutBase.Inner()), v[i, j]) for (i, j) in indices + ] + return GridLayoutSpec(pairs; kwargs...) +end + +GridLayoutSpec(contents...; kwargs...) = GridLayoutSpec([contents...]; kwargs...) + +""" +apply for return type PlotSpec +""" +function apply_convert!(P, attributes::Attributes, x::PlotSpec) + args, kwargs = x.args, x.kwargs + # Note that kw_args in the plot spec that are not part of the target plot type + # will end in the "global plot" kw_args (rest) + for (k, v) in pairs(kwargs) + attributes[k] = v + end + return (plottype(plottype(x), P), (args...,)) +end + +function apply_convert!(P, ::Attributes, x::AbstractVector{PlotSpec}) + return (PlotList, (x,)) +end + +""" +apply for return type + (args...,) +""" +apply_convert!(P, ::Attributes, x::Tuple) = (P, x) + +function MakieCore.argtypes(plot::PlotSpec) + args_converted = convert_arguments(plottype(plot), plot.args...) + return MakieCore.argtypes(args_converted) +end + +""" +See documentation for specapi. +""" +struct _SpecApi end +const SpecApi = _SpecApi() + +function Base.getproperty(::_SpecApi, field::Symbol) + field === :GridLayout && return GridLayoutSpec + # TODO, we wanted to track all recipe names in a set + # in MakieCore via the recipe macro, but due to precompilation & caching + # It seems impossible to merge the recipes from all modules + # Since precompilation will cache only MakieCore's state + # And once everything is compiled, and MakieCore is loaded into a package + # The names are loaded from cache and dont contain anything after MakieCore. + func = get_recipe_function(field) + if isnothing(func) + error("$(field) neither a recipe, Makie plotting object or a Block (like Axis, Legend, etc).") + elseif func isa Function + sym = plotsym(Plot{func}) + if (sym === :plot) # fallback for plotsym, so not found! + error("$(field) neither a recipe, Makie plotting object or a Block (like Axis, Legend, etc).") + end + @warn("PlotSpec objects are supposed to be title case. Found: $(field). Please use $(sym) instead.") + return (args...; kw...) -> PlotSpec(sym, args...; kw...) + elseif func <: Plot + return (args...; kw...) -> PlotSpec(field, args...; kw...) + elseif func <: Block + return (args...; kw...) -> BlockSpec(field, args...; kw...) + else + error("$(field) not a valid Block or Plot function") + end +end + +# We use this function to decide which plots to reuse + update instead of re-creating. +# Comparison based entirely of types inside args + kwargs. +# This will return false for the same plotspec with a new attribute +# E.g. `compare_spec(S.Scatter(1:4; color=:red), S.Scatter(1:4; marker=:circle))` +# While we could easily update this, we don't want to, since we're +# pessimistic about what's updatable and to avoid issues with +# Needing to reset attributes to their defaults, at the cost of re-creating more plots than necessary. +# TODO when focussing better performance, this is one of the first things we want to try +function compare_specs(a::PlotSpec, b::PlotSpec) + a.type === b.type || return false + length(a.args) == length(b.args) || return false + all(i-> typeof(a.args[i]) == typeof(b.args[i]), 1:length(a.args)) || return false + + length(a.kwargs) == length(b.kwargs) || return false + ka = keys(a.kwargs) + kb = keys(b.kwargs) + ka == kb || return false + all(k -> typeof(a.kwargs[k]) == typeof(b.kwargs[k]), ka) || return false + return true +end + +@inline function is_different(a, b) + # First check if they are the same object + # This disallows mutating PlotSpec arguments in place + a === b && return false + # If they're not the same objcets, we see if they contain the same values + a == b && return false + return true +end + +function update_plot!(obs_to_notify, plot::AbstractPlot, oldspec::PlotSpec, spec::PlotSpec) + # Update args in plot `input_args` list + for i in eachindex(spec.args) + # we should only call update_plot!, if compare_spec(spec_plot_got_created_from, spec) == true, + # Which should guarantee, that args + kwargs have the same length and types! + arg_obs = plot.args[i] + prev_val = oldspec.args[i] + if is_different(prev_val, spec.args[i]) # only update if different + arg_obs.val = spec.args[i] + push!(obs_to_notify, arg_obs) + end + end + scene = parent_scene(plot) + # Update attributes + for (attribute, new_value) in spec.kwargs + old_attr = plot[attribute] + # only update if different + if is_different(old_attr[], new_value) + if new_value isa Cycled + old_attr.val = to_color(scene, attribute, new_value) + else + @debug("updating kw $attribute") + old_attr.val = new_value + end + push!(obs_to_notify, old_attr) + end + end + # Cycling needs to be handled separately sadly, + # since they're implicitely mutating attributes, e.g. if I re-use a plot + # that has been on cycling position 2, and now I re-use it for the first plot in the list + # it will need to change to the color of cycling position 1 + if haskey(plot, :cycle) + cycle = get_cycle_for_plottype(plot.cycle[]) + uncycled = Set{Symbol}() + for (attr_vec, _) in cycle.cycle + for attr in attr_vec + if !haskey(spec.kwargs, attr) + push!(uncycled, attr) + end + end + end + + if !isempty(uncycled) + # remove all attributes that don't need cycling + for (attr_vec, _) in cycle.cycle + filter!(x -> x in uncycled, attr_vec) + end + add_cycle_attribute!(plot, scene, cycle) + append!(obs_to_notify, (plot[k] for k in uncycled)) + end + end + return +end + +""" + plotlist!( + [ + PlotSpec(:scatter, args...; kwargs...), + PlotSpec(:lines, args...; kwargs...), + ] + ) + +Plots a list of PlotSpec's, which can be an observable, making it possible to create efficiently animated plots with the following API: + +## Example +```julia +using GLMakie +import Makie.SpecApi as S + +fig = Figure() +ax = Axis(fig[1, 1]) +plots = Observable([S.heatmap(0 .. 1, 0 .. 1, Makie.peaks()), S.lines(0 .. 1, sin.(0:0.01:1); color=:blue)]) +pl = plot!(ax, plots) +display(fig) + +# Updating the plot dynamically +plots[] = [S.heatmap(0 .. 1, 0 .. 1, Makie.peaks()), S.lines(0 .. 1, sin.(0:0.01:1); color=:red)] +plots[] = [ + S.image(0 .. 1, 0 .. 1, Makie.peaks()), + S.poly(Rect2f(0.45, 0.45, 0.1, 0.1)), + S.lines(0 .. 1, sin.(0:0.01:1); linewidth=10, color=Makie.resample_cmap(:viridis, 101)), +] + +plots[] = [ + S.surface(0..1, 0..1, Makie.peaks(); colormap = :viridis, translation = Vec3f(0, 0, -1)), +] +``` +""" +@recipe(PlotList, plotspecs) do scene + Attributes() +end + +convert_arguments(::Type{<:AbstractPlot}, args::AbstractArray{<:PlotSpec}) = (args,) +plottype(::AbstractVector{PlotSpec}) = PlotList + +# Since we directly plot into the parent scene (hacky), we need to overload these +Base.insert!(::MakieScreen, ::Scene, ::PlotList) = nothing + +function Base.show(io::IO, ::MIME"text/plain", spec::PlotSpec) + args = join(map(x -> string("::", typeof(x)), spec.args), ", ") + kws = join([string(k, " = ", typeof(v)) for (k, v) in spec.kwargs], ", ") + println(io, "S.", spec.type, "($args; $kws)") + return +end + +function Base.show(io::IO, spec::PlotSpec) + args = join(map(x -> string("::", typeof(x)), spec.args), ", ") + kws = join([string(k, " = ", typeof(v)) for (k, v) in spec.kwargs], ", ") + println(io, "S.", spec.type, "($args; $kws)") + return +end + +function to_plot_object(ps::PlotSpec) + P = plottype(ps) + return P((ps.args...,), copy(ps.kwargs)) +end + +function find_reusable_plot(plotspec::PlotSpec, reusable_plots::IdDict{PlotSpec,Plot}) + for (spec, plot) in reusable_plots + if compare_specs(spec, plotspec) + return plot, spec + end + end + return nothing, nothing +end + +function diff_plotlist!(scene::Scene, plotspecs::Vector{PlotSpec}, obs_to_notify, reusable_plots, + plotlist::Union{Nothing,PlotList}=nothing) + new_plots = IdDict{PlotSpec,Plot}() # needed to be mutated + empty!(scene.cycler.counters) + # Global list of observables that need updating + # Updating them all at once in the end avoids problems with triggering updates while updating + # And at some point we may be able to optimize notify(list_of_observables) + empty!(obs_to_notify) + for plotspec in plotspecs + # we need to compare by types with compare_specs, since we can only update plots if the types of all attributes match + reused_plot, old_spec = find_reusable_plot(plotspec, reusable_plots) + if isnothing(reused_plot) + @debug("Creating new plot for spec") + # Create new plot, store it into our `cached_plots` dictionary + plot = plot!(scene, to_plot_object(plotspec)) + if !isnothing(plotlist) + push!(plotlist.plots, plot) + end + new_plots[plotspec] = plot + else + @debug("updating old plot with spec") + # Delete the plots from reusable_plots, so that we don't re-use it multiple times! + delete!(reusable_plots, old_spec) + update_plot!(obs_to_notify, reused_plot, old_spec, plotspec) + new_plots[plotspec] = reused_plot + end + end + return new_plots +end + +function update_plotspecs!(scene::Scene, list_of_plotspecs::Observable, plotlist::Union{Nothing, PlotList}=nothing) + # Cache plots here so that we aren't re-creating plots every time; + # if a plot still exists from last time, update it accordingly. + # If the plot is removed from `plotspecs`, we'll delete it from here + # and re-create it if it ever returns. + unused_plots = IdDict{PlotSpec,Plot}() + obs_to_notify = Observable[] + function update_plotlist(plotspecs) + # Global list of observables that need updating + # Updating them all at once in the end avoids problems with triggering updates while updating + # And at some point we may be able to optimize notify(list_of_observables) + empty!(obs_to_notify) + empty!(scene.cycler.counters) # Reset Cycler + # diff_plotlist! deletes all plots that get re-used from unused_plots + # so, this will become our list of unused plots! + new_plots = diff_plotlist!(scene, plotspecs, obs_to_notify, unused_plots, plotlist) + # Next, delete all plots that we haven't used + # TODO, we could just hide them, until we reach some max_plots_to_be_cached, so that we re-create less plots. + for (_, plot) in unused_plots + if !isnothing(plotlist) + filter!(x -> x !== plot, plotlist.plots) + end + delete!(scene, plot) + end + # Transfer all new plots into unused_plots for the next update! + @assert !any(x-> x in unused_plots, new_plots) + empty!(unused_plots) + merge!(unused_plots, new_plots) + # finally, notify all changes at once + foreach(notify, obs_to_notify) + return + end + l = Base.ReentrantLock() + on(scene, list_of_plotspecs; update=true) do plotspecs + lock(l) do + update_plotlist(plotspecs) + end + return + end + return +end + +function Makie.plot!(p::PlotList{<: Tuple{<: AbstractArray{PlotSpec}}}) + scene = Makie.parent_scene(p) + update_plotspecs!(scene, p[1], p) + return +end + +## BlockSpec + +function compare_layout_slot((anesting, ap, a)::Tuple{Int,GP,BlockSpec}, (bnesting, bp, b)::Tuple{Int,GP,BlockSpec}) where {GP<:GridLayoutPosition} + anesting !== bnesting && return false + a.type !== b.type && return false + ap !== bp && return false + return true +end + +function compare_layout_slot((anesting, ap, a)::Tuple{Int,GP, GridLayoutSpec}, (bnesting, bp, b)::Tuple{Int,GP, GridLayoutSpec}) where {GP <: GridLayoutPosition} + anesting !== bnesting && return false + ap !== bp && return false + for (ac, bc) in zip(a.content, b.content) + compare_layout_slot((anesting + 1, ac[1], ac[2]), (bnesting + 1, bc[1], bc[2])) || return false + end + return true +end + +compare_layout_slot(a, b) = false # types dont match + +function to_layoutable(parent, position::GridLayoutPosition, spec::BlockSpec) + BType = getfield(Makie, spec.type) + # TODO forward kw + block = BType(get_top_parent(parent); spec.kwargs...) + parent[position...] = block + return block +end + +function to_layoutable(parent, position::GridLayoutPosition, spec::GridLayoutSpec) + # TODO pass colsizes etc + gl = GridLayout(length(spec.rowsizes), length(spec.colsizes); + colsizes=spec.colsizes, + rowsizes=spec.rowsizes, + colgaps=spec.colgaps, + rowgaps=spec.rowgaps, + alignmode=spec.alignmode, + tellwidth=spec.tellwidth, + tellheight=spec.tellheight, + halign=spec.halign, + valign=spec.valign) + parent[position...] = gl + return gl +end + +function update_layoutable!(block::T, plot_obs, old_spec::BlockSpec, spec::BlockSpec) where T <: Block + old_attr = keys(old_spec.kwargs) + new_attr = keys(spec.kwargs) + # attributes that have been set previously and need to get unset now + reset_to_defaults = setdiff(old_attr, new_attr) + if !isempty(reset_to_defaults) + default_attrs = default_attribute_values(T, block.blockscene) + for attr in reset_to_defaults + setproperty!(block, attr, default_attrs[attr]) + end + end + # Attributes needing an update + to_update = setdiff(new_attr, reset_to_defaults) + for key in to_update + val = spec.kwargs[key] + prev_val = to_value(getproperty(block, key)) + if is_different(val, prev_val) + setproperty!(block, key, val) + end + end + # Reset the cycler + if hasproperty(block, :scene) + empty!(block.scene.cycler.counters) + end + if T <: AbstractAxis + plot_obs[] = spec.plots + scene = get_scene(block) + if any(needs_tight_limits, scene.plots) + tightlimits!(block) + end + end + return +end + +function to_gl_key(key::Symbol) + key === :colgaps && return :addedcolgaps + key === :rowgaps && return :addedrowgaps + return key +end + +function update_layoutable!(layout::GridLayout, obs, old_spec::Union{GridLayoutSpec, Nothing}, spec::GridLayoutSpec) + # Block updates until very end where all children etc got deleted! + layout.block_updates = true + keys = (:alignmode, :tellwidth, :tellheight, :halign, :valign) + layout.size = spec.size + layout.offsets = spec.offsets + for k in keys + # TODO! The gridlayout in the top parent figure has a padding from the Figure + # Since in the SpecApi we can do nested specs with whole figure, we can't create the default there since + # We don't know which GridLayout will be the main parent. + # So for now, we just ignore the padding for the top level gridlayout, since we assume the padding in the figurespec is wrong! + if layout.parent isa Figure && k == :alignmode + continue + end + old_val = isnothing(old_spec) ? nothing : getproperty(old_spec, k) + new_val = getproperty(spec, k) + if is_different(old_val, new_val) + value_obs = getfield(layout, k) + if value_obs isa Observable + value_obs[] = new_val + end + end + end + # TODO update colsizes etc + for field in [:size, :offsets, :colsizes, :rowsizes, :colgaps, :rowgaps] + old_val = isnothing(old_spec) ? nothing : getfield(old_spec, field) + new_val = getfield(spec, field) + if is_different(old_val, new_val) + setfield!(layout, to_gl_key(field), new_val) + end + end + return +end + +function find_layoutable(spec, layoutables) + for (i, (key, value)) in enumerate(layoutables) + if compare_layout_slot(key, spec) + return i, key, value + end + end + return 0, nothing, nothing +end + + +function update_gridlayout!(gridlayout::GridLayout, nesting::Int, oldgridspec::Union{Nothing, GridLayoutSpec}, + gridspec::GridLayoutSpec, previous_contents, new_layoutables) + + update_layoutable!(gridlayout, nothing, oldgridspec, gridspec) + + for (position, spec) in gridspec.content + # we need to compare by types with compare_specs, since we can only update plots if the types of all attributes match + idx, old_key, layoutable_obs = find_layoutable((nesting, position, spec), previous_contents) + if isnothing(layoutable_obs) + @debug("Creating new content for spec") + # Create new plot, store it into `new_layoutables` + new_layoutable = to_layoutable(gridlayout, position, spec) + obs = Observable(PlotSpec[]) + if new_layoutable isa AbstractAxis + obs = Observable(spec.plots) + scene = get_scene(new_layoutable) + update_plotspecs!(scene, obs) + if any(needs_tight_limits, scene.plots) + tightlimits!(new_layoutable) + end + update_state_before_display!(new_layoutable) + elseif new_layoutable isa GridLayout + # Make sure all plots & blocks are inserted + update_gridlayout!(new_layoutable, nesting + 1, spec, spec, previous_contents, + new_layoutables) + end + push!(new_layoutables, (nesting, position, spec) => (new_layoutable, obs)) + else + @debug("updating old block with spec") + # Make sure we don't double re-use a layoutable + splice!(previous_contents, idx) + (_, _, old_spec) = old_key + (layoutable, plot_obs) = layoutable_obs + gridlayout[position...] = layoutable + if layoutable isa GridLayout + update_gridlayout!(layoutable, nesting + 1, old_spec, spec, previous_contents, new_layoutables) + else + update_layoutable!(layoutable, plot_obs, old_spec, spec) + update_state_before_display!(layoutable) + end + # Carry over to cache it in new_layoutables + push!(new_layoutables, (nesting, position, spec) => (layoutable, plot_obs)) + end + end +end + +get_layout!(fig::Figure) = fig.layout +get_layout!(gp::Union{GridSubposition,GridPosition}) = GridLayoutBase.get_layout_at!(gp; createmissing=true) + +# We use this to decide if we can re-use a plot. +# (nesting_level_in_layout, position_in_layout, spec) +const LayoutableKey = Tuple{Int,GridLayoutPosition,LayoutableSpec} + +delete_layoutable!(block::Block) = delete!(block) +function delete_layoutable!(grid::GridLayout) + gc = grid.layoutobservables.gridcontent[] + if !isnothing(gc) + GridLayoutBase.remove_from_gridlayout!(gc) + end + return +end + +function update_fig!(fig::Union{Figure,GridPosition,GridSubposition}, layout_obs::Observable{GridLayoutSpec}) + # Global list of all layoutables. The LayoutableKey includes a nesting, so that we can keep even nested layouts in one global list. + # Vector of Pairs should allow to have an identical key without overwriting the previous value + unused_layoutables = Pair{LayoutableKey, Tuple{Layoutable,Observable{Vector{PlotSpec}}}}[] + new_layoutables = Pair{LayoutableKey,Tuple{Layoutable,Observable{Vector{PlotSpec}}}}[] + sizehint!(unused_layoutables, 50) + sizehint!(new_layoutables, 50) + l = Base.ReentrantLock() + layout = get_layout!(fig) + + on(get_topscene(fig), layout_obs; update=true) do layout_spec + lock(l) do + # For each update we look into `unused_layoutables` to see if we can re-use a layoutable (GridLayout/Block). + # Every re-used layoutable and every newly created gets pushed into `new_layoutables`, + # while it gets removed from `unused_layoutables`. + empty!(new_layoutables) + update_gridlayout!(layout, 1, nothing, layout_spec, unused_layoutables, new_layoutables) + # Everything that still is in unused_layoutables is not used anymore and can be deleted + for (key, (layoutable, obs)) in unused_layoutables + delete_layoutable!(layoutable) + Observables.clear(obs) + end + layouts_to_update = Set{GridLayout}([layout]) + for (_, (content, _)) in new_layoutables + if content isa GridLayout + push!(layouts_to_update, content) + else + gc = GridLayoutBase.gridcontent(content) + push!(layouts_to_update, gc.parent) + end + end + for l in layouts_to_update + l.block_updates = false + GridLayoutBase.update!(l) + end + # Finally transfer all new_layoutables into reusable_layoutables, + # since in the next update they will be the once we re-use + # TODO: Is this actually more efficent for GC then `reusable_layoutables=new_layoutables` ? + empty!(unused_layoutables) + append!(unused_layoutables, new_layoutables) + return + end + end + return fig +end + +args_preferred_axis(::GridLayoutSpec) = FigureOnly + +plot!(plot::Plot{MakieCore.plot,Tuple{GridLayoutSpec}}) = plot + +function plot!(fig::Union{Figure, GridLayoutBase.GridPosition}, plot::Plot{MakieCore.plot,Tuple{GridLayoutSpec}}) + figure = fig isa Figure ? fig : get_top_parent(fig) + connect_plot!(figure.scene, plot) + update_fig!(fig, plot[1]) + return fig +end + +function apply_convert!(P, attributes::Attributes, x::GridLayoutSpec) + return (Plot{plot}, (x,)) +end + +MakieCore.argtypes(::GridLayoutSpec) = Tuple{Nothing} diff --git a/src/stats/distributions.jl b/src/stats/distributions.jl index 0e219fb0bd9..6f2221119f3 100644 --- a/src/stats/distributions.jl +++ b/src/stats/distributions.jl @@ -113,7 +113,7 @@ maybefit(x, _) = x function convert_arguments(::Type{<:QQPlot}, x′, y; qqline = :none) x = maybefit(x′, y) points, line = fit_qqplot(x, y; qqline = qqline) - return PlotSpec{QQPlot}(points, line) + return PlotSpec(:QQPlot, points, line) end convert_arguments(::Type{<:QQNorm}, y; qqline = :none) = diff --git a/src/stats/ecdf.jl b/src/stats/ecdf.jl index 6494ca58df9..f8038456a9e 100644 --- a/src/stats/ecdf.jl +++ b/src/stats/ecdf.jl @@ -19,11 +19,13 @@ function convert_arguments(P::PlotFunc, ecdf::StatsBase.ECDF; npoints=10_000) end return to_plotspec(ptype, convert_arguments(ptype, x, ecdf(x)); kwargs...) end + function convert_arguments(P::PlotFunc, x::AbstractVector, ecdf::StatsBase.ECDF) ptype = plottype(P, Stairs) kwargs = ptype <: Stairs ? (; step=:post) : NamedTuple() return to_plotspec(ptype, convert_arguments(ptype, x, ecdf(x)); kwargs...) end + function convert_arguments(P::PlotFunc, x0::AbstractInterval, ecdf::StatsBase.ECDF) xmin, xmax = extrema(x0) z = ecdf_xvalues(ecdf, Inf) diff --git a/src/stats/hexbin.jl b/src/stats/hexbin.jl index 17501642235..c163f29bf0a 100644 --- a/src/stats/hexbin.jl +++ b/src/stats/hexbin.jl @@ -60,7 +60,7 @@ function spacings_offsets_nbins(bins, cellsizes::Tuple{<:Real,<:Real}, xmi, xma, ymi - (resty > 0 ? (yspacing - resty) / 2 : 0), Int(nx) + (restx > 0), Int(ny) + (resty > 0) end -Makie.conversion_trait(::Type{<:Hexbin}) = PointBased() +conversion_trait(::Type{<:Hexbin}) = PointBased() function data_limits(hb::Hexbin) bb = Rect3f(hb.plots[1][1][]) @@ -78,7 +78,7 @@ get_weight(weights, i) = Float64(weights[i]) get_weight(::StatsBase.UnitWeights, i) = 1e0 get_weight(::Nothing, i) = 1e0 -function Makie.plot!(hb::Hexbin{<:Tuple{<:AbstractVector{<:Point2}}}) +function plot!(hb::Hexbin{<:Tuple{<:AbstractVector{<:Point2}}}) xy = hb[1] points = Observable(Point2f[]) diff --git a/src/stats/hist.jl b/src/stats/hist.jl index a9f564270e6..5c943a88ea2 100644 --- a/src/stats/hist.jl +++ b/src/stats/hist.jl @@ -1,10 +1,10 @@ -const histogram_plot_types = [BarPlot, Heatmap, Volume] +const histogram_plot_types = (BarPlot, Heatmap, Volume) function convert_arguments(P::Type{<:AbstractPlot}, h::StatsBase.Histogram{<:Any, N}) where N ptype = plottype(P, histogram_plot_types[N]) f(edges) = edges[1:end-1] .+ diff(edges)./2 kwargs = N == 1 ? (; width = step(h.edges[1]), gap = 0, dodge_gap = 0) : NamedTuple() - to_plotspec(ptype, convert_arguments(ptype, map(f, h.edges)..., Float64.(h.weights)); kwargs...) + return to_plotspec(ptype, convert_arguments(ptype, map(f, h.edges)..., Float64.(h.weights)); kwargs...) end function _hist_center_weights(values, edges, normalization, scale_to, wgts) diff --git a/src/stats/violin.jl b/src/stats/violin.jl index ef6e6f5ff99..d3d7d5c9707 100644 --- a/src/stats/violin.jl +++ b/src/stats/violin.jl @@ -35,7 +35,7 @@ Draw a violin plot. ) end -conversion_trait(x::Type{<:Violin}) = SampleBased() +conversion_trait(::Type{<:Violin}) = SampleBased() getuniquevalue(v, idxs) = v diff --git a/src/theming.jl b/src/theming.jl index 79ed248882f..b8ee0924d4d 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -23,8 +23,6 @@ const DEFAULT_PALETTES = Attributes( side = [:left, :right] ) -Base.@deprecate_binding default_palettes DEFAULT_PALETTES - const MAKIE_DEFAULT_THEME = Attributes( palette = DEFAULT_PALETTES, font = :regular, @@ -34,16 +32,16 @@ const MAKIE_DEFAULT_THEME = Attributes( italic = "TeX Gyre Heros Makie Italic", bold_italic = "TeX Gyre Heros Makie Bold Italic", ), - fontsize = 16, + fontsize = 14, textcolor = :black, padding = Vec3f(0.05), figure_padding = 16, - rowgap = 24, - colgap = 24, + rowgap = 18, + colgap = 18, backgroundcolor = :white, colormap = :viridis, marker = :circle, - markersize = 12, + markersize = 9, markercolor = :black, markerstrokecolor = :black, markerstrokewidth = 0, @@ -53,7 +51,7 @@ const MAKIE_DEFAULT_THEME = Attributes( patchcolor = RGBAf(0, 0, 0, 0.6), patchstrokecolor = :black, patchstrokewidth = 0, - resolution = (800, 600), # 4/3 aspect ratio + size = (600, 450), # 4/3 aspect ratio visible = true, Axis = Attributes(), Axis3 = Attributes(), @@ -68,12 +66,23 @@ const MAKIE_DEFAULT_THEME = Attributes( blur = Int32(2), # A (2blur+1) by (2blur+1) range is used for blurring # N_samples = 64, # number of samples (requires shader reload) ), - ambient = RGBf(0.55, 0.55, 0.55), - lightposition = :eyeposition, inspectable = true, + # Vec is equvalent to 36° right/east, 39° up/north from camera position + # The order here is Vec3f(right of, up from, towards) viewer/camera + light_direction = Vec3f(-0.45679495, -0.6293204, -0.6287243), + camera_relative_light = true, # Only applies to default DirectionalLight + light_color = RGBf(0.5, 0.5, 0.5), + ambient = RGBf(0.35, 0.35, 0.35), + + # Note: this can be set too + # lights = AbstractLight[ + # AmbientLight(RGBf(0.55, 0.55, 0.55)), + # DirectionalLight(RGBf(0.8, 0.8, 0.8), Vec3f(2/3, 2/3, 1/3)) + # ], + CairoMakie = Attributes( - px_per_unit = 1.0, + px_per_unit = 2.0, pt_per_unit = 0.75, antialias = :best, visible = true, @@ -87,6 +96,8 @@ const MAKIE_DEFAULT_THEME = Attributes( vsync = false, render_on_demand = true, framerate = 30.0, + px_per_unit = automatic, + scalefactor = automatic, # GLFW window attributes float = false, @@ -98,19 +109,27 @@ const MAKIE_DEFAULT_THEME = Attributes( monitor = nothing, visible = true, - # Postproccessor + # Shader constants & Postproccessor oit = true, fxaa = true, ssao = false, # This adjusts a factor in the rendering shaders for order independent # transparency. This should be the same for all of them (within one rendering # pipeline) otherwise depth "order" will be broken. - transparency_weight_scale = 1000f0 + transparency_weight_scale = 1000f0, + # maximum number of lights with shading = :verbose + max_lights = 64, + max_light_parameters = 5 * 64 ), WGLMakie = Attributes( framerate = 30.0, - resize_to_body = false + resize_to = nothing, + # DEPRECATED in favor of resize_to + # still needs to be here to gracefully deprecate it + resize_to_body = nothing, + px_per_unit = automatic, + scalefactor = automatic ), RPRMakie = Attributes( @@ -121,9 +140,6 @@ const MAKIE_DEFAULT_THEME = Attributes( ) ) -Base.@deprecate_binding minimal_default MAKIE_DEFAULT_THEME - - const CURRENT_DEFAULT_THEME = deepcopy(MAKIE_DEFAULT_THEME) const THEME_LOCK = Base.ReentrantLock() @@ -144,6 +160,26 @@ function merge_without_obs!(result::Attributes, theme::Attributes) end return result end + +# Same as above, but second argument gets priority so, `merge_without_obs_reverse!(Attributes(a=22), Attributes(a=33)) -> Attributes(a=33)` +function merge_without_obs_reverse!(result::Attributes, priority::Attributes) + result_dict = attributes(result) + for (key, value) in priority + if !haskey(result_dict, key) + result_dict[key] = Observable{Any}(to_value(value)) # the deepcopy part for observables + else + current_value = result[key] + if value isa Attributes && current_value isa Attributes + # if nested attribute, we merge recursively + merge_without_obs_reverse!(current_value, value) + else + result_dict[key] = Observable{Any}(to_value(value)) + end + end + end + return result +end + # Use copy with no obs to quickly deepcopy fast_deepcopy(attributes) = merge_without_obs!(Attributes(), attributes) @@ -177,7 +213,7 @@ restored afterwards, no matter if `f` succeeds or fails. Example: ```julia -my_theme = Theme(resolution = (500, 500), color = :red) +my_theme = Theme(size = (500, 500), color = :red) with_theme(my_theme, color = :blue, linestyle = :dashed) do scatter(randn(100, 2)) end @@ -198,6 +234,7 @@ function with_theme(f, theme = Theme(); kwargs...) end theme(::Nothing, key::Symbol; default=nothing) = theme(key; default) +theme(::Nothing) = CURRENT_DEFAULT_THEME function theme(key::Symbol; default=nothing) if haskey(CURRENT_DEFAULT_THEME, key) val = to_value(CURRENT_DEFAULT_THEME[key]) diff --git a/src/types.jl b/src/types.jl index b91dd552cce..a3144183a7b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,4 +1,6 @@ abstract type AbstractCamera end +abstract type Block end +abstract type AbstractAxis <: Block end # placeholder if no camera is present struct EmptyCamera <: AbstractCamera end @@ -194,6 +196,14 @@ function Base.empty!(events::Events) return end +abstract type BooleanOperator end + +""" + IsPressedInputType + +Union containing possible input types for `ispressed`. +""" +const IsPressedInputType = Union{Bool,BooleanOperator,Mouse.Button,Keyboard.Button,Set,Vector,Tuple} """ Camera(pixel_area) @@ -231,7 +241,12 @@ struct Camera resolution::Observable{Vec2f} """ - Eye position of the camera, sued for e.g. ray tracing. + Focal point of the camera, used for e.g. camera synchronized light direction. + """ + lookat::Observable{Vec3f} + + """ + Eye position of the camera, used for e.g. ray tracing. """ eyeposition::Observable{Vec3f} @@ -240,6 +255,8 @@ struct Camera We need to keep track of them, so, that we can connect and disconnect them. """ steering_nodes::Vector{ObserverFunction} + + calculated_values::Dict{Symbol, Observable} end """ @@ -253,41 +270,48 @@ struct Transformation <: Transformable scale::Observable{Vec3f} rotation::Observable{Quaternionf} model::Observable{Mat4f} + parent_model::Observable{Mat4f} # data conversion observable, for e.g. log / log10 etc transform_func::Observable{Any} - function Transformation(translation, scale, rotation, model, transform_func) - return new( - RefValue{Transformation}(), - translation, scale, rotation, model, transform_func - ) + function Transformation(translation, scale, rotation, transform_func) + translation_o = convert(Observable{Vec3f}, translation) + scale_o = convert(Observable{Vec3f}, scale) + rotation_o = convert(Observable{Quaternionf}, rotation) + parent_model = Observable(Mat4f(I)) + model = map(translation_o, scale_o, rotation_o, parent_model) do t, s, r, p + return p * transformationmatrix(t, s, r) + end + transform_func_o = convert(Observable{Any}, transform_func) + return new(RefValue{Transformation}(), + translation_o, scale_o, rotation_o, model, parent_model, transform_func_o) end end -""" -`PlotSpec{P<:AbstractPlot}(args...; kwargs...)` - -Object encoding positional arguments (`args`), a `NamedTuple` of attributes (`kwargs`) -as well as plot type `P` of a basic plot. -""" -struct PlotSpec{P<:AbstractPlot} - args::Tuple - kwargs::NamedTuple - PlotSpec{P}(args...; kwargs...) where {P<:AbstractPlot} = new{P}(args, values(kwargs)) +function Transformation(transform_func=identity; + scale=Vec3f(1), + translation=Vec3f(0), + rotation=Quaternionf(0, 0, 0, 1)) + return Transformation(translation, + scale, + rotation, + transform_func) end -PlotSpec(args...; kwargs...) = PlotSpec{Combined{Any}}(args...; kwargs...) - -Base.getindex(p::PlotSpec, i::Int) = getindex(p.args, i) -Base.getindex(p::PlotSpec, i::Symbol) = getproperty(p.kwargs, i) - -to_plotspec(::Type{P}, args; kwargs...) where {P} = - PlotSpec{P}(args...; kwargs...) - -to_plotspec(::Type{P}, p::PlotSpec{S}; kwargs...) where {P, S} = - PlotSpec{plottype(P, S)}(p.args...; p.kwargs..., kwargs...) - -plottype(::PlotSpec{P}) where {P} = P - +function Transformation(parent::Transformable; + scale=Vec3f(1), + translation=Vec3f(0), + rotation=Quaternionf(0, 0, 0, 1), + transform_func=nothing) + connect_func = isnothing(transform_func) + trans = isnothing(transform_func) ? identity : transform_func + + trans = Transformation(translation, + scale, + rotation, + trans) + connect!(transformation(parent), trans; connect_func=connect_func) + return trans +end struct ScalarOrVector{T} sv::Union{T, Vector{T}} @@ -430,3 +454,10 @@ end (s::ReversibleScale)(args...) = s.forward(args...) # functor Base.show(io::IO, s::ReversibleScale) = print(io, "ReversibleScale($(s.name))") Base.show(io::IO, ::MIME"text/plain", s::ReversibleScale) = print(io, "ReversibleScale($(s.name))") + + +struct Cycler + counters::IdDict{Type,Int} +end + +Cycler() = Cycler(IdDict{Type,Int}()) diff --git a/src/units.jl b/src/units.jl index d93d71f8331..7105410199e 100644 --- a/src/units.jl +++ b/src/units.jl @@ -18,7 +18,7 @@ ######### function to_screen(scene::Scene, mpos) - return Point2f(mpos) .- Point2f(minimum(pixelarea(scene)[])) + return Point2f(mpos) .- Point2f(minimum(viewport(scene)[])) end number(x::Unit) = x.value diff --git a/src/utilities/quaternions.jl b/src/utilities/quaternions.jl index bcae57f50de..f5ccf832bc3 100644 --- a/src/utilities/quaternions.jl +++ b/src/utilities/quaternions.jl @@ -81,6 +81,17 @@ function Base.:(*)(quat::Quaternion{T}, vec::P) where {T, P <: StaticVector{3}} (num8 - num11) * vec[1] + (num9 + num10) * vec[2] + (1f0 - (num4 + num5)) * vec[3] ) end + +function Base.:(*)(quat::Quaternion, bb::Rect3{T}) where {T} + points = corners(bb) + first = points[1] + bb = Ref(Rect3{T}(quat * first, zero(first))) + for i in 2:length(points) + bb[] = _update_rect(bb[], Point3{T}(quat * points[i])) + end + return bb[] +end + Base.conj(q::Quaternion) = Quaternion(-q[1], -q[2], -q[3], q[4]) function Base.:(*)(q::Quaternion, w::Quaternion) diff --git a/src/utilities/texture_atlas.jl b/src/utilities/texture_atlas.jl index 352353c5f50..133d3344373 100644 --- a/src/utilities/texture_atlas.jl +++ b/src/utilities/texture_atlas.jl @@ -1,4 +1,4 @@ -const SERIALIZATION_FORMAT_VERSION = "v5" +const SERIALIZATION_FORMAT_VERSION = "v6" struct TextureAtlas rectangle_packer::RectanglePacker{Int32} @@ -157,7 +157,7 @@ function get_texture_atlas(resolution::Int = 2048, pix_per_glyph::Int = 64) end end -const CACHE_DOWNLOAD_URL = "https://github.com/MakieOrg/Makie.jl/releases/download/v0.19.0/" +const CACHE_DOWNLOAD_URL = "https://github.com/MakieOrg/Makie.jl/releases/download/v0.20.0/" function cached_load(resolution::Int, pix_per_glyph::Int) path = get_cache_path(resolution, pix_per_glyph) @@ -187,8 +187,6 @@ end const DEFAULT_FONT = NativeFont[] const ALTERNATIVE_FONTS = NativeFont[] const FONT_LOCK = Base.ReentrantLock() -Base.@deprecate_binding _default_font DEFAULT_FONT -Base.@deprecate_binding _alternative_fonts ALTERNATIVE_FONTS function defaultfont() lock(FONT_LOCK) do @@ -291,19 +289,26 @@ function glyph_uv_width!(atlas::TextureAtlas, b::BezierPath) return atlas.uv_rectangles[glyph_index!(atlas, b)] end + +# Seems like StableHashTraits is so slow, that it's worthwhile to memoize the hashes +const MEMOIZED_HASHES = Dict{Any, UInt32}() + +function fast_stable_hash(x) + return get!(MEMOIZED_HASHES, x) do + return StableHashTraits.stable_hash(x; alg=crc32c, version=2) + end +end + function insert_glyph!(atlas::TextureAtlas, glyph, font::NativeFont) glyphindex = FreeTypeAbstraction.glyph_index(font, glyph) - hash = StableHashTraits.stable_hash((glyphindex, FreeTypeAbstraction.fontname(font)); - alg=crc32c, version=2) + hash = fast_stable_hash((glyphindex, FreeTypeAbstraction.fontname(font))) return insert_glyph!(atlas, hash, (glyphindex, font)) end function insert_glyph!(atlas::TextureAtlas, path::BezierPath) - return insert_glyph!(atlas, StableHashTraits.stable_hash(path; alg=crc32c, version=2), - path) + return insert_glyph!(atlas, fast_stable_hash(path), path) end - function insert_glyph!(atlas::TextureAtlas, hash::UInt32, path_or_glyp::Union{BezierPath, Tuple{UInt64, NativeFont}}) return get!(atlas.mapping, hash) do uv_pixel = render(atlas, path_or_glyp) @@ -434,6 +439,7 @@ function marker_to_sdf_shape(arr::AbstractVector) shape1 = marker_to_sdf_shape(first(arr)) for elem in arr shape2 = marker_to_sdf_shape(elem) + shape2 isa Shape && shape1 isa Shape && continue shape1 !== shape2 && error("Can't use an array of markers that require different primitive_shapes $(typeof.(arr)).") end return shape1 @@ -552,10 +558,11 @@ end offset_marker(atlas, marker, font, markersize, markeroffset) = markeroffset -function marker_attributes(atlas::TextureAtlas, marker, markersize, font, marker_offset) +function marker_attributes(atlas::TextureAtlas, marker, markersize, font, marker_offset, plot_object) atlas_obs = Observable(atlas) # for map to work - scale = map(rescale_marker, atlas_obs, marker, font, markersize; ignore_equal_values=true) - quad_offset = map(offset_marker, atlas_obs, marker, font, markersize, marker_offset; ignore_equal_values=true) + scale = map(rescale_marker, plot_object, atlas_obs, marker, font, markersize; ignore_equal_values=true) + quad_offset = map(offset_marker, plot_object, atlas_obs, marker, font, markersize, marker_offset; + ignore_equal_values=true) return scale, quad_offset end diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index 1db23b13cbc..fd2d653eb56 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -35,23 +35,6 @@ function resample_cmap(cmap, ncolors::Integer; alpha=1.0) end end -""" - resampled_colors(attributes::Attributes, levels::Integer) - -Resample the color attribute from `attributes`. Resamples `:colormap` if present, -or repeats `:color`. -""" -function resampled_colors(attributes, levels::Integer) - cols = if haskey(attributes, :color) - c = get_attribute(attributes, :color) - c isa AbstractVector ? resample(c, levels) : repeated(c, levels) - else - c = get_attribute(attributes, :colormap) - resample(c, levels) - end -end - - """ Like `get!(f, dict, key)` but also calls `f` and replaces `key` when the corresponding value is nothing @@ -106,7 +89,19 @@ function extract_expr(extract_func, dictlike, args) end """ -usage @extract scene (a, b, c, d) + @extract scene (a, b, c, d) + +This becomes + +```julia +begin + a = scene[:a] + b = scene[:b] + c = scene[:d] + d = scene[:d] + (a, b, c, d) +end +``` """ macro extract(scene, args) extract_expr(getindex, scene, args) @@ -184,6 +179,7 @@ attr_broadcast_getindex(x::Ref, i) = x[] # unwrap Refs just like in normal broad attr_broadcast_getindex(x::ScalarOrVector, i) = x.sv isa Vector ? x.sv[i] : x.sv is_vector_attribute(x::AbstractVector) = true +is_vector_attribute(x::Base.Generator) = is_vector_attribute(x.iter) is_vector_attribute(x::NativeFont) = false is_vector_attribute(x::Quaternion) = false is_vector_attribute(x::VecTypes) = false @@ -266,6 +262,22 @@ function merged_get!(defaults::Function, key, scene::SceneLike, input::Attribute return merge!(input, d) end +function Base.replace!(target::Attributes, key, scene::SceneLike, overwrite::Attributes) + if haskey(theme(scene), key) + _replace!(target, theme(scene, key)) + end + return _replace!(target, overwrite) +end + +function _replace!(target::Attributes, overwrite::Attributes) + for k in keys(target) + haskey(overwrite, k) && (target[k] = overwrite[k]) + end + return +end + + + to_vector(x::AbstractVector, len, T) = convert(Vector{T}, x) function to_vector(x::AbstractArray, len, T) if length(x) in size(x) # assert that just one dim != 1 @@ -320,6 +332,80 @@ function surface_normals(x, y, z) return vec(map(normal, CartesianIndices(z))) end + +############################################################ +# NaN-aware normal & mesh handling # +############################################################ + +""" + nan_aware_orthogonal_vector(v1, v2, v3) where N + +Returns an un-normalized normal vector for the triangle formed by the three input points. +Skips any combination of the inputs for which any point has a NaN component. +""" +function nan_aware_orthogonal_vector(v1, v2, v3) + (isnan(v1) || isnan(v2) || isnan(v3)) && return Vec3f(0) + return Vec3f(cross(v2 - v1, v3 - v1)) +end + +""" + nan_aware_normals(vertices::AbstractVector{<: Union{Point, PointMeta}}, faces::AbstractVector{F}) + +Computes the normals of a mesh defined by `vertices` and `faces` (a vector of `GeometryBasics.NgonFace`) +which ignores all contributions from points with `NaN` components. + +Equivalent in application to `GeometryBasics.normals`. +""" +function nan_aware_normals(vertices::AbstractVector{<:AbstractPoint{3,T}}, faces::AbstractVector{F}) where {T,F<:NgonFace} + normals_result = zeros(Vec3f, length(vertices)) + free_verts = GeometryBasics.metafree.(vertices) + + for face in faces + + v1, v2, v3 = free_verts[face] + # we can get away with two edges since faces are planar. + n = nan_aware_orthogonal_vector(v1, v2, v3) + + for i in 1:length(F) + fi = face[i] + normals_result[fi] = normals_result[fi] + n + end + end + normals_result .= GeometryBasics.normalize.(normals_result) + return normals_result +end + +function nan_aware_normals(vertices::AbstractVector{<:AbstractPoint{2,T}}, faces::AbstractVector{F}) where {T,F<:NgonFace} + return Vec2f.(nan_aware_normals(map(v -> Point3{T}(v..., 0), vertices), faces)) +end + + +function nan_aware_normals(vertices::AbstractVector{<:GeometryBasics.PointMeta{D,T}}, faces::AbstractVector{F}) where {D,T,F<:NgonFace} + return nan_aware_normals(collect(GeometryBasics.metafree.(vertices)), faces) +end + +function surface2mesh(xs, ys, zs::AbstractMatrix, transform_func = identity, space = :data) + # crate a `Matrix{Point3}` + # ps = matrix_grid(identity, xs, ys, zs) + ps = matrix_grid(p -> apply_transform(transform_func, p, space), xs, ys, zs) + # create valid tessellations (triangulations) for the mesh + # knowing that it is a regular grid makes this simple + rect = Tesselation(Rect2f(0, 0, 1, 1), size(zs)) + # we use quad faces so that color handling is consistent + faces = decompose(QuadFace{Int}, rect) + # and remove quads that contain a NaN coordinate to avoid drawing triangles + faces = filter(f -> !any(i -> isnan(ps[i]), f), faces) + # create the uv (texture) vectors + uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), decompose_uv(rect)) + # return a mesh with known uvs and normals. + return GeometryBasics.Mesh(GeometryBasics.meta(ps; uv=uv, normals = nan_aware_normals(ps, faces)), faces, ) +end + + +############################################################ +# Matrix grid method for surface handling # +############################################################ + """ matrix_grid(f, x::AbstractArray, y::AbstractArray, z::AbstractMatrix)::Vector{Point3f} @@ -337,6 +423,10 @@ function matrix_grid(f, x::ClosedInterval, y::ClosedInterval, z::AbstractMatrix) matrix_grid(f, LinRange(extrema(x)..., size(z, 1)), LinRange(extrema(x)..., size(z, 2)), z) end +############################################################ +# Attribute key extraction # +############################################################ + function extract_keys(attributes, keys) attr = Attributes() for key in keys @@ -346,5 +436,21 @@ function extract_keys(attributes, keys) end # Scalar - Vector getindex -sv_getindex(v::Vector, i::Integer) = v[i] -sv_getindex(x, i::Integer) = x +sv_getindex(v::AbstractVector, i::Integer) = v[i] +sv_getindex(x, ::Integer) = x +sv_getindex(x::VecTypes, ::Integer) = x + +# TODO: move to GeometryBasics +function corners(rect::Rect2{T}) where T + o = minimum(rect) + w = widths(rect) + T0 = zero(T) + return Point{3,T}[o .+ Vec2{T}(x, y) for x in (T0, w[1]) for y in (T0, w[2])] +end + +function corners(rect::Rect3{T}) where T + o = minimum(rect) + w = widths(rect) + T0 = zero(T) + return Point{3,T}[o .+ Vec3{T}(x, y, z) for x in (T0, w[1]) for y in (T0, w[2]) for z in (T0, w[3])] +end diff --git a/test/boundingboxes.jl b/test/boundingboxes.jl index 99fbf8cdd57..37aa126331e 100644 --- a/test/boundingboxes.jl +++ b/test/boundingboxes.jl @@ -29,13 +29,24 @@ end fig, ax, p = surface([x*y for x in 1:10, y in 1:10]) bb = boundingbox(p) - @test bb.origin ≈ Point3f(0.0, 0.0, 1.0) - @test bb.widths ≈ Vec3f(10.0, 10.0, 99.0) + @test bb.origin ≈ Point3f(1.0, 1.0, 1.0) + @test bb.widths ≈ Vec3f(9.0, 9.0, 99.0) fig, ax, p = meshscatter([Point3f(x, y, z) for x in 1:5 for y in 1:5 for z in 1:5]) bb = boundingbox(p) - @test bb.origin ≈ Point3f(1) - @test bb.widths ≈ Vec3f(4) + # Note: awkwards numbers come from using mesh over Sphere + @test bb.origin ≈ Point3f(0.9011624, 0.9004657, 0.9) + @test bb.widths ≈ Vec3f(4.1986046, 4.199068, 4.2) + + fig, ax, p = meshscatter( + [Point3f(0) for _ in 1:3], + marker = Rect3f(Point3f(-0.1, -0.1, -0.1), Vec3f(0.2, 0.2, 1.2)), + markersize = Vec3f(1, 1, 2), + rotations = Makie.rotation_between.((Vec3f(0,0,1),), Vec3f[(1,0,0), (0,1,0), (0,0,1)]) + ) + bb = boundingbox(p) + @test bb.origin ≈ Point3f(-0.2) + @test bb.widths ≈ Vec3f(2.4) fig, ax, p = volume(rand(5, 5, 5)) bb = boundingbox(p) @@ -68,10 +79,10 @@ end @test bb.widths ≈ Vec3f(10.0, 10.0, 0) # text transforms to pixel space atm (TODO) - fig = Figure(resolution = (400, 400)) + fig = Figure(size = (400, 400)) ax = Axis(fig[1, 1]) p = text!(ax, Point2f(10), text = "test", fontsize = 20) bb = boundingbox(p) - @test bb.origin ≈ Point3f(340, 341, 0) + @test bb.origin ≈ Point3f(343.0, 345.0, 0) @test bb.widths ≈ Vec3f(32.24, 23.3, 0) end \ No newline at end of file diff --git a/test/conversions.jl b/test/conversions.jl index c735ed4735b..bb9a9961974 100644 --- a/test/conversions.jl +++ b/test/conversions.jl @@ -38,7 +38,6 @@ end X4 = rand(2,10) V4 = to_vertices(X4) @test Float32(X4[1,7]) == V4[7][1] - @test V4[7][3] == 0 X5 = rand(3,10) V5 = to_vertices(X5) @@ -47,7 +46,6 @@ end X6 = rand(10,2) V6 = to_vertices(X6) @test Float32(X6[7,1]) == V6[7][1] - @test V6[7][3] == 0 X7 = rand(10,3) V7 = to_vertices(X7) @@ -117,8 +115,7 @@ end @testset "functions" begin x = -pi..pi - s = convert_arguments(Lines, x, sin) - xy = s.args[1] + (xy,) = convert_arguments(Lines, x, sin) @test xy[1][1] ≈ -pi @test xy[end][1] ≈ pi for (val, fval) in xy @@ -126,8 +123,7 @@ end end x = range(-pi, stop=pi, length=100) - s = convert_arguments(Lines, x, sin) - xy = s.args[1] + (xy,) = convert_arguments(Lines, x, sin) @test xy[1][1] ≈ -pi @test xy[end][1] ≈ pi for (val, fval) in xy @@ -303,6 +299,68 @@ end @test pl.plots[1][1][] == Makie.poly_convert(points) end +@testset "GridBased and ImageLike conversions" begin + # type tree + @test GridBased <: ConversionTrait + @test CellGrid <: GridBased + @test VertexGrid <: GridBased + @test ImageLike <: ConversionTrait + + # Plot to trait + @test conversion_trait(Image) === ImageLike() + @test conversion_trait(Heatmap) === CellGrid() + @test conversion_trait(Surface) === VertexGrid() + @test conversion_trait(Contour) === VertexGrid() + @test conversion_trait(Contourf) === VertexGrid() + + m1 = [x for x in 1:10, y in 1:6] + m2 = [y for x in 1:10, y in 1:6] + m3 = rand(10, 6) + + r1 = 1:10 + r2 = 1:6 + + v1 = collect(1:10) + v2 = collect(1:6) + + i1 = 1..10 + i2 = 1..6 + + o3 = Float32.(m3) + + # Conversions + @testset "ImageLike conversion" begin + @test convert_arguments(Image, m3) == (0f0..10f0, 0f0..6f0, o3) + @test convert_arguments(Image, v1, r2, m3) == (1f0..10f0, 1f0..6f0, o3) + @test convert_arguments(Image, i1, v2, m3) == (1f0..10f0, 1f0..6f0, o3) + @test_throws ErrorException convert_arguments(Image, m1, m2, m3) + @test_throws ErrorException convert_arguments(Heatmap, m1, m2) + end + + @testset "VertexGrid conversion" begin + vo1 = Float32.(v1) + vo2 = Float32.(v2) + mo1 = Float32.(m1) + mo2 = Float32.(m2) + @test convert_arguments(Surface, m3) == (vo1, vo2, o3) + @test convert_arguments(Contour, i1, v2, m3) == (vo1, vo2, o3) + @test convert_arguments(Contourf, v1, r2, m3) == (vo1, vo2, o3) + @test convert_arguments(Surface, m1, m2, m3) == (mo1, mo2, o3) + @test convert_arguments(Surface, m1, m2) == (mo1, mo2, zeros(Float32, size(o3))) + end + + @testset "CellGrid conversion" begin + o1 = Float32.(0.5:1:10.5) + o2 = Float32.(0.5:1:6.5) + @test convert_arguments(Heatmap, m3) == (o1, o2, o3) + @test convert_arguments(Heatmap, r1, i2, m3) == (o1, o2, o3) + @test convert_arguments(Heatmap, v1, r2, m3) == (o1, o2, o3) + @test convert_arguments(Heatmap, 0:10, v2, m3) == (collect(0f0:10f0), o2, o3) + @test_throws ErrorException convert_arguments(Heatmap, m1, m2, m3) + @test_throws ErrorException convert_arguments(Heatmap, m1, m2) + end +end + @testset "Triplot" begin xs = rand(Float32, 10) ys = rand(Float32, 10) @@ -382,4 +440,4 @@ end # sanity checks @test isapprox(Makie.angle2align(pi/4), Vec2f(1, 1), atol = 1e-12) @test isapprox(Makie.angle2align(5pi/4), Vec2f(0, 0), atol = 1e-12) -end \ No newline at end of file +end diff --git a/test/deprecated.jl b/test/deprecated.jl new file mode 100644 index 00000000000..0af22226632 --- /dev/null +++ b/test/deprecated.jl @@ -0,0 +1,49 @@ +# @test_deprecated seems broken on 1.9 + 1.10 +macro depwarn_message(expr) + quote + logger = Test.TestLogger() + Base.with_logger(logger) do + $(esc(expr)) + end + if length(logger.logs) == 1 + return logger.logs[1].message + else + return nothing + end + end +end + +@testset "deprecations" begin + @testset "Scene" begin + # test that deprecated `resolution keyword still works but throws warning` + logger = Test.TestLogger() + Base.with_logger(logger) do + scene = Scene(; resolution=(999, 999), size=(123, 123)) + @test scene.viewport[] == Rect2i((0, 0), (999, 999)) + end + @test occursin("The `resolution` keyword for `Scene`s and `Figure`s has been deprecated", + logger.logs[1].message) + scene = Scene(; size=(600, 450)) + msg = @depwarn_message scene.px_area + @test occursin(".px_area` got renamed to `.viewport`, and means the area the scene maps to in device indepentent units", + msg) + # @test_deprecated seems to be broken on 1.10?! + msg = @depwarn_message pixelarea(scene) + # only works with depwarn on + @test occursin("`pixelarea` is deprecated, use `viewport` instead.", msg) + end + @testset "Plot -> Combined" begin + logger = Test.TestLogger() + msg = @depwarn_message Combined + @test occursin("Combined is deprecated", msg) + @test Combined == Plot + end + @testset "Surface Traits" begin + @test DiscreteSurface == CellGrid + @test ContinuousSurface == VertexGrid + msg = @depwarn_message DiscreteSurface() + @test occursin("DiscreteSurface is deprecated", msg) + msg = @depwarn_message ContinuousSurface() + @test occursin("ContinuousSurface is deprecated", msg) + end +end diff --git a/test/events.jl b/test/events.jl index 7a8491080ac..41b7a94d440 100644 --- a/test/events.jl +++ b/test/events.jl @@ -135,7 +135,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right clipboard() = CLIP[] end @testset "copy_paste" begin - f = Figure(resolution=(640,480)) + f = Figure(size=(640,480)) tb = Textbox(f[1,1], placeholder="Copy/paste into me") e = events(f.scene) @@ -166,7 +166,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # Refresh figure to test right control + v combination empty!(f) - f = Figure(resolution=(640,480)) + f = Figure(size=(640,480)) tb = Textbox(f[1,1], placeholder="Copy/paste into me") e = events(f.scene) @@ -196,11 +196,11 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # This testset is based on the results the current camera system has. If # cam3d! is updated this is likely to break. @testset "cam3d!" begin - scene = Scene(resolution=(800, 600)); + scene = Scene(size=(800, 600)); e = events(scene) cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false) cc = cameracontrols(scene) - + # Verify initial camera state @test cc.lookat[] == Vec3f(0) @test cc.eyeposition[] == Vec3f(3) @@ -218,20 +218,20 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # 2) Outside scene, in drag e.mouseposition[] = (1000, 450) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491522) + @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491514) @test cc.upvector[] ≈ Vec3f(-0.5050875, -0.6730229, 0.5403024) # 3) not in drag e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.release) e.mouseposition[] = (400, 250) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491522) + @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491514) @test cc.upvector[] ≈ Vec3f(-0.5050875, -0.6730229, 0.5403024) # Reset state so this is indepentent from the last checks - scene = Scene(resolution=(800, 600)); + scene = Scene(size=(800, 600)); e = events(scene) cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false) cc = cameracontrols(scene) @@ -243,29 +243,30 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # translation # 1) In scene, in drag + e.mouseposition[] = (400, 250) e.mousebutton[] = MouseButtonEvent(Mouse.right, Mouse.press) e.mouseposition[] = (600, 250) - @test cc.lookat[] ≈ Vec3f(5.4697413, -3.3484206, -2.1213205) - @test cc.eyeposition[] ≈ Vec3f(8.469742, -0.34842062, 0.8786795) + @test cc.lookat[] ≈ Vec3f(1.0146117, -1.0146117, 0.0) + @test cc.eyeposition[] ≈ Vec3f(4.0146117, 1.9853883, 3.0) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) # 2) Outside scene, in drag e.mouseposition[] = (1000, 450) - @test cc.lookat[] ≈ Vec3f(9.257657, -5.4392805, -3.818377) - @test cc.eyeposition[] ≈ Vec3f(12.257658, -2.4392805, -0.81837714) + @test cc.lookat[] ≈ Vec3f(3.6296215, -2.4580488, -1.1715729) + @test cc.eyeposition[] ≈ Vec3f(6.6296215, 0.5419513, 1.8284271) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) # 3) not in drag e.mousebutton[] = MouseButtonEvent(Mouse.right, Mouse.release) e.mouseposition[] = (400, 250) - @test cc.lookat[] ≈ Vec3f(9.257657, -5.4392805, -3.818377) - @test cc.eyeposition[] ≈ Vec3f(12.257658, -2.4392805, -0.81837714) + @test cc.lookat[] ≈ Vec3f(3.6296215, -2.4580488, -1.1715729) + @test cc.eyeposition[] ≈ Vec3f(6.6296215, 0.5419513, 1.8284271) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) # Reset state - scene = Scene(resolution=(800, 600)); + scene = Scene(size=(800, 600)); e = events(scene) cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false) cc = cameracontrols(scene) @@ -274,26 +275,24 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right @test cc.lookat[] == Vec3f(0) @test cc.eyeposition[] == Vec3f(3) @test cc.upvector[] == Vec3f(0, 0, 1) - @test cc.zoom_mult[] == 1f0 # Zoom + e.mouseposition[] = (400, 250) # for debugging e.scroll[] = (0.0, 4.0) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(3) + @test cc.eyeposition[] ≈ 0.6830134f0 * Vec3f(3) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) - @test cc.zoom_mult[] ≈ 0.6830134f0 # should not work outside the scene e.mouseposition[] = (1000, 450) e.scroll[] = (0.0, 4.0) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(3) + @test cc.eyeposition[] ≈ 0.6830134f0 * Vec3f(3) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) - @test cc.zoom_mult[] ≈ 0.6830134f0 end @testset "mouse state machine" begin - scene = Scene(resolution=(800, 600)); + scene = Scene(size=(800, 600)); e = events(scene) bbox = Observable(Rect2(200, 200, 400, 300)) msm = addmouseevents!(scene, bbox, priority=typemax(Int)) @@ -444,7 +443,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # TODO: test more @testset "Axis Interactions" begin - f = Figure(resolution = (400, 400)) + f = Figure(size = (400, 400)) a = Axis(f[1, 1]) e = events(f) diff --git a/test/figures.jl b/test/figures.jl index 4d844d115af..de8c1e8a315 100644 --- a/test/figures.jl +++ b/test/figures.jl @@ -155,8 +155,8 @@ end end @testset "Figure and axis kwargs validation" begin - @test_throws ArgumentError lines(1:10, axis = (aspect = DataAspect()), figure = (resolution = (100, 100))) - @test_throws ArgumentError lines(1:10, figure = (resolution = (100, 100))) + @test_throws ArgumentError lines(1:10, axis = (aspect = DataAspect()), figure = (size = (100, 100))) + @test_throws ArgumentError lines(1:10, figure = (size = (100, 100))) @test_throws ArgumentError lines(1:10, axis = (aspect = DataAspect())) # these just shouldn't error diff --git a/test/pipeline.jl b/test/pipeline.jl index 9412face4ff..e54a1a52055 100644 --- a/test/pipeline.jl +++ b/test/pipeline.jl @@ -30,15 +30,89 @@ end xmax = Observable{Any}([0.25, 0.5, 0.75, 1]) p = hlines!(ax, list, xmax = xmax, color = :blue) - @test getfield(p, :input_args)[1] === list + @test getfield(p, :args)[1] === list @test p.xmax === xmax fig end + +@testset "Figure / Axis / Gridposition creation test" begin + @testset "proper errors for wrongly used (non) mutating plot functions" begin + f = Figure() + x = range(0, 10, length=100) + @test_throws ErrorException scatter!(f[1, 1], x, sin) + @test_throws ErrorException scatter!(f[1, 2][1, 1], x, sin) + @test_throws ErrorException scatter!(f[1, 2][1, 2], x, sin) + + @test_throws ErrorException meshscatter!(f[2, 1], x, sin; axis=(type=Axis3,)) + @test_throws ErrorException meshscatter!(f[2, 2][1, 1], x, sin; axis=(type=Axis3,)) + @test_throws ErrorException meshscatter!(f[2, 2][1, 2], x, sin; axis=(type=Axis3,)) + + @test_throws ErrorException meshscatter!(f[3, 1], rand(Point3f, 10); axis=(type=LScene,)) + @test_throws ErrorException meshscatter!(f[3, 2][1, 1], rand(Point3f, 10); axis=(type=LScene,)) + @test_throws ErrorException meshscatter!(f[3, 2][1, 2], rand(Point3f, 10); axis=(type=LScene,)) + + sub = f[4, :] + f = Figure() + @test_throws ErrorException scatter(Axis(f[1, 1]), x, sin) + @test_throws ErrorException meshscatter(Axis3(f[1, 1]), x, sin) + @test_throws ErrorException meshscatter(LScene(f[1, 1]), rand(Point3f, 10)) + + f + end + + @testset "creating plot object for different (non) mutating plotting functions into figure" begin + f = Figure() + x = range(0, 10; length=100) + ax, pl = scatter(f[1, 1], x, sin) + @test ax isa Axis + @test pl isa AbstractPlot + + ax, pl = scatter(f[1, 2][1, 1], x, sin) + @test ax isa Axis + @test pl isa AbstractPlot + + ax, pl = scatter(f[1, 2][1, 2], x, sin) + @test ax isa Axis + @test pl isa AbstractPlot + + ax, pl = meshscatter(f[2, 1], x, sin; axis=(type=Axis3,)) + @test ax isa Axis3 + @test pl isa AbstractPlot + + ax, pl = meshscatter(f[2, 2][1, 1], x, sin; axis=(type=Axis3,)) + @test ax isa Axis3 + @test pl isa AbstractPlot + ax, pl = meshscatter(f[2, 2][1, 2], x, sin; axis=(type=Axis3,)) + @test ax isa Axis3 + @test pl isa AbstractPlot + + ax, pl = meshscatter(f[3, 1], rand(Point3f, 10); axis=(type=LScene,)) + @test ax isa LScene + @test pl isa AbstractPlot + ax, pl = meshscatter(f[3, 2][1, 1], rand(Point3f, 10); axis=(type=LScene,)) + @test ax isa LScene + @test pl isa AbstractPlot + ax, pl = meshscatter(f[3, 2][1, 2], rand(Point3f, 10); axis=(type=LScene,)) + @test ax isa LScene + @test pl isa AbstractPlot + + sub = f[4, :] + + pl = scatter!(Axis(sub[1, 1]), x, sin) + @test pl isa AbstractPlot + pl = meshscatter!(Axis3(sub[1, 2]), x, sin) + @test pl isa AbstractPlot + pl = meshscatter!(LScene(sub[1, 3]), rand(Point3f, 10)) + @test pl isa AbstractPlot + + end +end + @testset "Cycled" begin # Test for https://github.com/MakieOrg/Makie.jl/issues/3266 f, ax, pl = lines(1:4; color=Cycled(2)) - cpalette = ax.palette[:color][] + cpalette = ax.scene.theme.palette[:color][] @test pl.calculated_colors[] == cpalette[2] pl2 = lines!(ax, 1:4; color=Cycled(1)) @test pl2.calculated_colors[] == cpalette[1] diff --git a/test/ray_casting.jl b/test/ray_casting.jl index ec88508ba18..73b5beaa20a 100644 --- a/test/ray_casting.jl +++ b/test/ray_casting.jl @@ -1,7 +1,7 @@ @testset "Ray Casting" begin @testset "View Rays" begin scene = Scene() - xy = 0.5 * widths(pixelarea(scene)[]) + xy = 0.5 * widths(viewport(scene)[]) orthographic_cam3d!(x) = cam3d!(x, perspectiveprojection = Makie.Orthographic) @@ -20,13 +20,13 @@ end - # transform() is used to apply a translation-rotation-scale matrix to rays + # 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) @@ -71,76 +71,76 @@ end - # Note that these tests depend on the exact placement of plots and may + # 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)) + scene = Scene(size = (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)) + + scene = Scene(size = (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)) + scene = Scene(size = (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)) + + scene = Scene(size = (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)) + scene = Scene(size = (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)) + scene = Scene(size = (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)) + scene = Scene(size = (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) + @test pos ≈ Point3f(10.0, 0.08616829, 9.989262) end # For recreating the above: - #= + #= # Scene setup from tests: - scene = Scene(resolution = (400, 400)) + scene = Scene(size = (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 @@ -160,4 +160,4 @@ scene =# -end \ No newline at end of file +end diff --git a/test/runtests.jl b/test/runtests.jl index 24de4e41329..929f8d4d473 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,6 +18,8 @@ using Makie: volume @test all(hi .>= (8,8,10)) end + include("deprecated.jl") + include("specapi.jl") include("primitives.jl") include("pipeline.jl") include("record.jl") diff --git a/test/scenes.jl b/test/scenes.jl index b28a6dd51b8..a2c26e96b92 100644 --- a/test/scenes.jl +++ b/test/scenes.jl @@ -7,3 +7,59 @@ @test theme(nothing, :nonexistant, default=1) == 1 @test theme(scene, :nonexistant, default=1) == 1 end + +@testset "Lighting" begin + @testset "Shading default" begin + plot = (attributes = Attributes(), ) # simplified "plot" + + # Based on number of lights + lights = Makie.AbstractLight[] + Makie.default_shading!(plot, lights) + @test !haskey(plot.attributes, :shading) + + plot.attributes[:shading] = Observable(Makie.automatic) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === NoShading + + plot.attributes[:shading] = Observable(Makie.automatic) + push!(lights, AmbientLight(RGBf(0.1, 0.1, 0.1))) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === FastShading + + plot.attributes[:shading] = Observable(Makie.automatic) + push!(lights, DirectionalLight(RGBf(0.1, 0.1, 0.1), Vec3f(1))) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === FastShading + + plot.attributes[:shading] = Observable(Makie.automatic) + push!(lights, PointLight(RGBf(0.1, 0.1, 0.1), Point3f(0))) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + # Based on light types + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [SpotLight(RGBf(0.1, 0.1, 0.1), Point3f(0), Vec3f(1), Vec2f(0.2, 0.3))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [EnvironmentLight(1.0, rand(2,2))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === NoShading # only affects RPRMakie so skipped here + + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [PointLight(RGBf(0.1, 0.1, 0.1), Point3f(0))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [PointLight(RGBf(0.1, 0.1, 0.1), Point3f(0), Vec2f(0.1, 0.2))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + # keep existing shading type + lights = Makie.AbstractLight[] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + end +end diff --git a/test/specapi.jl b/test/specapi.jl new file mode 100644 index 00000000000..3cff1e024ba --- /dev/null +++ b/test/specapi.jl @@ -0,0 +1,73 @@ +import Makie.SpecApi as S + +@testset "diffing" begin + @testset "update_plot!" begin + obs = Observable[] + oldspec = S.Scatter(1:4; cycle=[]) + newspec = S.Scatter(1:4; cycle=[]) + p = Makie.to_plot_object(newspec) + s = Scene() + plot!(s, p) + Makie.update_plot!(obs, p, oldspec, newspec) + @test isempty(obs) + + newspec = S.Scatter(1:4; color=:red) + Makie.update_plot!(obs, p, oldspec, newspec) + oldspec = newspec + @test length(obs) == 1 + @test obs[1] === p.color + + newspec = S.Scatter(1:4; color=:green, cycle=[]) + empty!(obs) + Makie.update_plot!(obs, p, oldspec, newspec) + oldspec = newspec + @test length(obs) == 1 + @test obs[1] === p.color + @test obs[1].val == to_color(:green) + + newspec = S.Scatter(1:5; color=:green, cycle=[]) + empty!(obs) + Makie.update_plot!(obs, p, oldspec, newspec) + oldspec = newspec + @test length(obs) == 1 + @test obs[1] === p.args[1] + + oldspec = S.Scatter(1:5; color=:green, marker=:rect, cycle=[]) + newspec = S.Scatter(1:4; color=:red, marker=:circle, cycle=[]) + empty!(obs) + p = Makie.to_plot_object(oldspec) + s = Scene() + plot!(s, p) + Makie.update_plot!(obs, p, oldspec, newspec) + @test length(obs) == 3 + @test obs[1] === p.args[1] + @test obs[2] === p.color + @test obs[3] === p.marker + end + + @testset "diff_plotlist!" begin + scene = Scene(); + plotspecs = [S.Scatter(1:4; color=:red), S.Scatter(1:4; color=:red)] + reusable_plots = IdDict{PlotSpec,Plot}() + obs_to_notify = Observable[] + new_plots = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, reusable_plots) + @test length(new_plots) == 2 + @test Set(scene.plots) == Set(values(new_plots)) + @test isempty(obs_to_notify) + + new_plots2 = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, new_plots) + + @test isempty(new_plots) # they got all used up + @test Set(scene.plots) == Set(values(new_plots2)) + @test isempty(obs_to_notify) + + plotspecs = [S.Scatter(1:4; color=:yellow), S.Scatter(1:4; color=:green)] + new_plots3 = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, new_plots2) + + @test isempty(new_plots) # they got all used up + @test Set(scene.plots) == Set(values(new_plots3)) + # TODO, and some point we should try to find the matching plot and just + # switch them, so we don't need an update! + @test Set(obs_to_notify) == Set([scene.plots[1].color, scene.plots[2].color]) + end +end diff --git a/test/zoom_pan.jl b/test/zoom_pan.jl index 8a5a3e5daa7..5fd708e80df 100644 --- a/test/zoom_pan.jl +++ b/test/zoom_pan.jl @@ -4,7 +4,7 @@ using Observables function cleanaxes() fig = Figure() ax = Axis(fig[1, 1]) - axbox = pixelarea(ax.scene)[] + axbox = viewport(ax.scene)[] lim = ax.finallimits[] e = events(ax.scene) return ax, axbox, lim, e @@ -79,7 +79,7 @@ end fig = Figure() ax = Axis(fig[1, 1]) plot!(ax, [10, 15, 20]) - axbox = pixelarea(ax.scene)[] + axbox = viewport(ax.scene)[] lim = ax.finallimits[] e = events(ax.scene)