diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index 0243c706209..004aabc7cfd 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -23,4 +23,4 @@ jobs: - name: CompatHelper.main() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: julia -e 'using CompatHelper; CompatHelper.main()' + run: julia -e 'using CompatHelper; CompatHelper.main(; subdirs=["", "CairoMakie", "GLMakie", "MakieCore", "WGLMakie"])' diff --git a/.github/workflows/cairomakie.yaml b/.github/workflows/cairomakie.yaml new file mode 100644 index 00000000000..6e4e4c8f4bf --- /dev/null +++ b/.github/workflows/cairomakie.yaml @@ -0,0 +1,63 @@ +name: CairoMakie CI +on: + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' + branches: + - master + push: + paths-ignore: + - 'docs/**' + - '*.md' + branches: + - master + tags: '*' +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - '1.5' + - '1.6' + os: + - ubuntu-latest + arch: + - x64 + steps: + - name: Checkout + uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - name: Install Julia dependencies + shell: julia --project=monorepo {0} + run: | + using Pkg; + # dev mono repo versions + pkg"dev . MakieCore CairoMakie ReferenceTests" + - name: Run the tests + run: julia --project=monorepo -e 'using Pkg; Pkg.test("CairoMakie", coverage=true)' + - name: Upload test Artifacts + uses: actions/upload-artifact@v2 + with: + name: ReferenceImages + path: ./test/recorded + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e501852688f..d06f805cb7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: Makie CI on: pull_request: paths-ignore: @@ -21,7 +21,7 @@ jobs: fail-fast: false matrix: version: - - '1.3' + - '1.5' - '1.6' os: - ubuntu-latest @@ -44,8 +44,14 @@ jobs: ${{ runner.os }}-test-${{ env.cache-name }}- ${{ runner.os }}-test- ${{ runner.os }}- - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 + - name: Install Julia dependencies + shell: julia --project=monorepo {0} + run: | + using Pkg; + # dev mono repo versions + pkg"dev . MakieCore" + Pkg.test("Makie"; coverage=true) + - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v1 with: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a99b2419098..c9d72cf7ce8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -47,8 +47,8 @@ jobs: # force the most recent registry to avoid older cached versions pkg"registry add https://github.com/JuliaRegistries/General"; pkg"registry up General"; - Pkg.develop(PackageSpec(path=pwd())); - pkg"add Documenter#master; instantiate"' + # dev mono repo versions + pkg"dev . MakieCore CairoMakie WGLMakie GLMakie; add Documenter#master; instantiate"' - name: Build and deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/glmakie.yaml b/.github/workflows/glmakie.yaml new file mode 100644 index 00000000000..4c09d030bba --- /dev/null +++ b/.github/workflows/glmakie.yaml @@ -0,0 +1,66 @@ +name: GLMakie CI +on: + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' + branches: + - master + push: + paths-ignore: + - 'docs/**' + - '*.md' + branches: + - master + tags: '*' +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - '1.5' + - '1.6' + os: + - ubuntu-18.04 + arch: + - x64 + steps: + - name: Checkout + uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev + - name: Install Julia dependencies + shell: julia --project=monorepo {0} + run: | + using Pkg; + # dev mono repo versions + pkg"dev . MakieCore GLMakie ReferenceTests" + - name: Run the tests + run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=monorepo -e 'using Pkg; Pkg.test("GLMakie", coverage=true)' + - name: Upload test Artifacts + uses: actions/upload-artifact@v2 + with: + name: ReferenceImages + path: | + ./test/recorded + ./test/recorded_glmakie + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info diff --git a/.github/workflows/wglmakie.yaml b/.github/workflows/wglmakie.yaml new file mode 100644 index 00000000000..f4e862abafa --- /dev/null +++ b/.github/workflows/wglmakie.yaml @@ -0,0 +1,64 @@ +name: WGLMakie CI +on: + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' + branches: + - master + push: + paths-ignore: + - 'docs/**' + - '*.md' + branches: + - master + tags: '*' +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - '1.5' + - '1.6' + os: + - ubuntu-latest + arch: + - x64 + steps: + - name: Checkout + uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev + - name: Install Julia dependencies + shell: julia --project=monorepo {0} + run: | + using Pkg; + # dev mono repo versions + pkg"dev . MakieCore WGLMakie ReferenceTests" + - name: Run the tests + run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=monorepo -e 'using Pkg; Pkg.test("WGLMakie", coverage=true)' + - name: Upload test Artifacts + uses: actions/upload-artifact@v2 + with: + name: ReferenceImages + path: ./test/recorded + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info diff --git a/.gitignore b/.gitignore index 012b5eb77f5..adf87ed9a82 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,13 @@ src/.cache src/logo.png **/._* Manifest.toml -!docs/Manifest.toml /build \.DS_Store +CairoMakie/src/display.* + +WGLMakie/test/recorded/ +GLMakie/test/recorded/ +GLMakie/test/recorded_glmakie/ +CairoMakie/test/recorded/ +monorepo diff --git a/CairoMakie/LICENSE.md b/CairoMakie/LICENSE.md new file mode 100644 index 00000000000..7b4c161480b --- /dev/null +++ b/CairoMakie/LICENSE.md @@ -0,0 +1,22 @@ +The CairoMakie.jl package is licensed under the MIT "Expat" License: + +> Copyright (c) 2017: SimonDanisch. +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all +> copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +> SOFTWARE. +> diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml new file mode 100644 index 00000000000..dd3c4e1bb7e --- /dev/null +++ b/CairoMakie/Project.toml @@ -0,0 +1,36 @@ +name = "CairoMakie" +uuid = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +author = ["Simon Danisch "] +version = "0.6" + +[deps] +Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +FreeType = "b38be410-82b0-50bf-ab77-7b57e271db43" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[compat] +Cairo = "1.0.4" +Colors = "0.10, 0.11, 0.12" +FFTW = "1" +FileIO = "1.1" +FreeType = "3, 4.0" +GeometryBasics = "0.2, 0.3" +Makie = "=0.14.0" +StaticArrays = "0.12, 1.0" +julia = "1.3" + +[extras] +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +ReferenceTests = "d37af2e0-5618-4e00-9939-d430db56ee94" + +[targets] +test = ["Test", "Pkg", "ReferenceTests"] diff --git a/CairoMakie/README.md b/CairoMakie/README.md new file mode 100644 index 00000000000..d15c1c02e1a --- /dev/null +++ b/CairoMakie/README.md @@ -0,0 +1,49 @@ +# CairoMakie + +The Cairo Backend for Makie + +Read the docs for Makie and it's backends [here](http://makie.juliaplots.org/.dev) + + +## Issues + +Please file all issues in [Makie.jl](https://github.com/JuliaPlots/Makie.jl/issues/new), and mention CairoMakie in the issue text. + +## Limitations + +CairoMakie is intended as a backend for static vector graphics at publication quality. Therefore, it does not support the interactive features of GLMakie and is slower when visualizing large amounts data. 3D plots are currently not available because of the inherent limitations of 2D vector graphics. + +## Saving + +Makie overloads the FileIO interface, so you can save a Scene `scene` as `save("filename.extension", scene)`. CairoMakie supports saving to PNG, PDF, SVG and EPS. + +You can scale the size of the output figure, without changing its appearance by passing keyword arguments to `save`. PNGs can be scaled by `px_per_unit` (default 1) and vector graphics (SVG, PDF, EPS) can be scaled by `pt_per_unit`. + +```julia +save("plot.svg", scene, pt_per_unit = 0.5) # halve the dimensions of the resulting SVG +save("plot.png", scene, px_per_unit = 2) # double the resolution of the resulting PNG +``` + +## Using CairoMakie with Gtk.jl + +You can render onto a GtkCanvas using Gtk, and use that as a display for your scenes. + +```julia +using Gtk, CairoMakie, Makie + +canvas = @GtkCanvas() +window = GtkWindow(canvas, "Makie", 500, 500) + +function drawonto(canvas, scene) + @guarded draw(canvas) do _ + resize!(scene, Gtk.width(canvas), Gtk.height(canvas)) + screen = CairoMakie.CairoScreen(scene, Gtk.cairo_surface(canvas), getgc(canvas), nothing) + CairoMakie.cairo_draw(screen, scene) + end +end + +scene = heatmap(rand(50, 50)) # or something + +drawonto(canvas, scene) +show(canvas); # trigger rendering +``` diff --git a/CairoMakie/src/CairoMakie.jl b/CairoMakie/src/CairoMakie.jl new file mode 100644 index 00000000000..35f42a85467 --- /dev/null +++ b/CairoMakie/src/CairoMakie.jl @@ -0,0 +1,62 @@ +module CairoMakie + +using Makie, LinearAlgebra +using Colors, GeometryBasics, FileIO, StaticArrays +import SHA +import Base64 +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 +using Makie: to_value, to_colormap, extrema_nan +using Makie: inline! + +const OneOrVec{T} = Union{ + T, + Vec{N1, T} where N1, + NTuple{N2, T} where N2, +} + +# re-export Makie +for name in names(Makie) + @eval using Makie: $(name) + @eval export $(name) +end +export inline! + +include("infrastructure.jl") +include("utils.jl") +include("fonts.jl") +include("primitives.jl") +include("overrides.jl") + +function __init__() + activate!() + Makie.register_backend!(Makie.current_backend[]) +end + +function display_path(type::String) + if !(type in ("svg", "png", "pdf", "eps")) + error("Only \"svg\", \"png\", \"eps\" and \"pdf\" are allowed for `type`. Found: $(type)") + end + return joinpath(@__DIR__, "display." * type) +end + +const _last_inline = Ref(true) +const _last_type = Ref("png") +const _last_px_per_unit = Ref(1.0) +const _last_pt_per_unit = Ref(0.75) + +function activate!(; inline = _last_inline[], type = _last_type[], px_per_unit=_last_px_per_unit[], pt_per_unit=_last_pt_per_unit[]) + backend = CairoBackend(display_path(type); px_per_unit=px_per_unit, pt_per_unit=pt_per_unit) + Makie.current_backend[] = backend + Makie.use_display[] = !inline + _last_inline[] = inline + _last_type[] = type + _last_px_per_unit[] = px_per_unit + _last_pt_per_unit[] = pt_per_unit + return +end + +end diff --git a/CairoMakie/src/display.svg b/CairoMakie/src/display.svg new file mode 100644 index 00000000000..92a2aef83e5 --- /dev/null +++ b/CairoMakie/src/display.svg @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CairoMakie/src/fonts.jl b/CairoMakie/src/fonts.jl new file mode 100644 index 00000000000..169fb56c9c7 --- /dev/null +++ b/CairoMakie/src/fonts.jl @@ -0,0 +1,48 @@ +function set_font_matrix(ctx, matrix) + ccall((:cairo_set_font_matrix, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, Ref(matrix)) +end + +function get_font_matrix(ctx) + matrix = Cairo.CairoMatrix() + ccall((:cairo_get_font_matrix, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, Ref(matrix)) + return matrix +end + +function cairo_font_face_destroy(font_face) + ccall( + (:cairo_font_face_destroy, Cairo.libcairo), + Cvoid, (Ptr{Cvoid},), + font_face + ) +end + +function set_ft_font(ctx, font) + + font_face = ccall( + (:cairo_ft_font_face_create_for_ft_face, Cairo.libcairo), + Ptr{Cvoid}, (Makie.FreeTypeAbstraction.FT_Face, Cint), + font, 0 + ) + + ccall((:cairo_set_font_face, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, font_face) + + return font_face +end + + + +""" +Finds a font that can represent the unicode character! +Returns Makie.defaultfont() if not representable! +""" +function best_font(c::Char, font = Makie.defaultfont()) + if Makie.FreeType.FT_Get_Char_Index(font, c) == 0 + for afont in Makie.alternativefonts() + if Makie.FreeType.FT_Get_Char_Index(afont, c) != 0 + return afont + end + end + return Makie.defaultfont() + end + return font +end diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl new file mode 100644 index 00000000000..52d89ae060b --- /dev/null +++ b/CairoMakie/src/infrastructure.jl @@ -0,0 +1,366 @@ +#################################################################################################### +# Infrastructure # +#################################################################################################### + +################################################################################ +# Types # +################################################################################ + +@enum RenderType SVG PNG PDF EPS + +"The Cairo backend object. Used to dispatch to CairoMakie methods." +struct CairoBackend <: Makie.AbstractBackend + typ::RenderType + path::String + px_per_unit::Float64 + pt_per_unit::Float64 +end + +""" + struct CairoScreen{S} <: AbstractScreen +A "screen" type for CairoMakie, which encodes a surface +and a context which are used to draw a Scene. +""" +struct CairoScreen{S} <: Makie.AbstractScreen + scene::Scene + surface::S + context::Cairo.CairoContext + pane::Nothing # TODO: GtkWindowLeaf +end + + +function CairoBackend(path::String; px_per_unit=1, pt_per_unit=1) + ext = splitext(path)[2] + typ = if ext == ".png" + PNG + elseif ext == ".svg" + SVG + elseif ext == ".pdf" + PDF + elseif ext == ".eps" + EPS + else + error("Unsupported extension: $ext") + end + CairoBackend(typ, path, px_per_unit, pt_per_unit) +end + +# we render the scene directly, since we have +# no screen dependent state like in e.g. opengl +Base.insert!(screen::CairoScreen, scene::Scene, plot) = nothing + +function Base.show(io::IO, ::MIME"text/plain", screen::CairoScreen{S}) where S + println(io, "CairoScreen{$S} with surface:") + println(io, screen.surface) +end + +# Default to ARGB Surface as backing device +# TODO: integrate Gtk into this, so we can have an interactive display +""" + CairoScreen(scene::Scene; antialias = Cairo.ANTIALIAS_BEST) +Create a CairoScreen backed by an image surface. +""" +function CairoScreen(scene::Scene; device_scaling_factor = 1, antialias = Cairo.ANTIALIAS_BEST) + w, h = round.(Int, scene.camera.resolution[] .* device_scaling_factor) + surf = Cairo.CairoARGBSurface(w, h) + + # this sets a scaling factor on the lowest level that is "hidden" so its even + # enabled when the drawing space is reset for strokes + # that means it can be used to increase or decrease the image resolution + ccall((:cairo_surface_set_device_scale, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble, Cdouble), + surf.ptr, device_scaling_factor, device_scaling_factor) + + ctx = Cairo.CairoContext(surf) + Cairo.set_antialias(ctx, antialias) + + return CairoScreen(scene, surf, ctx, nothing) +end + +function get_type(surface::Cairo.CairoSurface) + return ccall((:cairo_surface_get_type, Cairo.libcairo), Cint, (Ptr{Nothing},), surface.ptr) +end + +is_vector_backend(ctx::Cairo.CairoContext) = is_vector_backend(ctx.surface) + +function is_vector_backend(surf::Cairo.CairoSurface) + typ = get_type(surf) + return typ in (Cairo.CAIRO_SURFACE_TYPE_PDF, Cairo.CAIRO_SURFACE_TYPE_PS, Cairo.CAIRO_SURFACE_TYPE_SVG) +end + +""" + CairoScreen( + scene::Scene, path::Union{String, IO}, mode::Symbol; + antialias = Cairo.ANTIALIAS_BEST + ) +Creates a CairoScreen pointing to a given output path, with some rendering type defined by `mode`. +""" +function CairoScreen(scene::Scene, path::Union{String, IO}, mode::Symbol; device_scaling_factor = 1, antialias = Cairo.ANTIALIAS_BEST) + + # the surface size is the scene size scaled by the device scaling factor + w, h = round.(Int, scene.camera.resolution[] .* device_scaling_factor) + + if mode == :svg + surf = Cairo.CairoSVGSurface(path, w, h) + elseif mode == :pdf + surf = Cairo.CairoPDFSurface(path, w, h) + elseif mode == :eps + surf = Cairo.CairoEPSSurface(path, w, h) + elseif mode == :png + surf = Cairo.CairoARGBSurface(w, h) + else + error("No available Cairo surface for mode $mode") + end + + # this sets a scaling factor on the lowest level that is "hidden" so its even + # enabled when the drawing space is reset for strokes + # that means it can be used to increase or decrease the image resolution + ccall((:cairo_surface_set_device_scale, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble, Cdouble), + surf.ptr, device_scaling_factor, device_scaling_factor) + + ctx = Cairo.CairoContext(surf) + Cairo.set_antialias(ctx, antialias) + + return CairoScreen(scene, surf, ctx, nothing) +end + + +function Base.delete!(screen::CairoScreen, scene::Scene, plot::AbstractPlot) + # Currently, we rerender every time, so nothing needs + # to happen here. However, in the event that changes, + # e.g. if we integrate a Gtk window, we may need to + # do something here. +end + +"Convert a rendering type to a MIME type" +function to_mime(x::RenderType) + x == SVG && return MIME("image/svg+xml") + x == PDF && return MIME("application/pdf") + x == EPS && return MIME("application/postscript") + return MIME("image/png") +end +to_mime(x::CairoBackend) = to_mime(x.typ) + +################################################################################ +# Rendering pipeline # +################################################################################ + +######################################## +# Drawing pipeline # +######################################## + +# The main entry point into the drawing pipeline +function cairo_draw(screen::CairoScreen, scene::Scene) + Makie.update!(scene) + draw_background(screen, scene) + + allplots = get_all_plots(scene) + sort!(allplots, by = zvalue) + + last_scene = scene + + Cairo.save(screen.context) + for p in allplots + to_value(get(p, :visible, true)) || continue + # only prepare for scene when it changes + # this should reduce the number of unnecessary clipping masks etc. + if p.parent != last_scene + Cairo.restore(screen.context) + Cairo.save(screen.context) + prepare_for_scene(screen, p.parent) + last_scene = p.parent + end + Cairo.save(screen.context) + draw_plot(p.parent, screen, p) + Cairo.restore(screen.context) + end + + return +end + +# this is a simplification which will only really work with non-rotated or +# scaled scene transformations +# but for Cairo's 2D paradigm that is the only likely mode of transformation +# and this way we can use the z-value as a means to shift the drawing order +# by translating e.g. the axis spines forward so they are not obscured halfway +# by heatmaps or images +zvalue(x) = Makie.translation(x)[][3] + zvalue(x.parent) +zvalue(::Nothing) = 0f0 + +function get_all_plots(scene, plots = AbstractPlot[]) + append!(plots, scene.plots) + for c in scene.children + get_all_plots(c, plots) + end + plots +end + +function prepare_for_scene(screen::CairoScreen, 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)[] + scene_height = widths(scene_area)[2] + scene_x_origin, scene_y_origin = scene_area.origin + + # we need to translate x by the origin, so distance from the left + # but y by the distance from the top, which is not the origin, but can + # be calculated using the parent's height, the scene's height and the y origin + # this is because y goes downwards in Cairo and upwards in Makie + + 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 + Cairo.rectangle(screen.context, 0, 0, widths(scene_area)...) + Cairo.clip(screen.context) + + return +end + +function draw_background(screen::CairoScreen, scene::Scene) + cr = screen.context + Cairo.save(cr) + if scene.clear[] + bg = to_color(theme(scene, :backgroundcolor)[]) + Cairo.set_source_rgba(cr, red(bg), green(bg), blue(bg), alpha(bg)); + r = pixelarea(scene)[] + Cairo.rectangle(cr, origin(r)..., widths(r)...) # background + fill(cr) + end + Cairo.restore(cr) + foreach(child_scene-> draw_background(screen, child_scene), scene.children) +end + +function draw_plot(scene::Scene, screen::CairoScreen, primitive::Combined) + if to_value(get(primitive, :visible, true)) + if isempty(primitive.plots) + Cairo.save(screen.context) + draw_atomic(scene, screen, primitive) + Cairo.restore(screen.context) + else + for plot in primitive.plots + draw_plot(scene, screen, plot) + end + end + end + return +end + +function draw_atomic(::Scene, ::CairoScreen, x) + @warn "$(typeof(x)) is not supported by cairo right now" +end + +function clear(screen::CairoScreen) + ctx = screen.ctx + Cairo.save(ctx) + Cairo.set_operator(ctx, Cairo.OPERATOR_SOURCE) + Cairo.set_source_rgba(ctx, rgbatuple(screen.scene[:backgroundcolor])...); + Cairo.paint(ctx) + Cairo.restore(ctx) +end + +######################################### +# Backend interface to Makie # +######################################### + +function Makie.backend_display(x::CairoBackend, scene::Scene) + return open(x.path, "w") do io + Makie.backend_show(x, io, to_mime(x), scene) + end +end + +Makie.backend_showable(x::CairoBackend, ::MIME"image/svg+xml", scene::Scene) = x.typ == SVG +Makie.backend_showable(x::CairoBackend, ::MIME"application/pdf", scene::Scene) = x.typ == PDF +Makie.backend_showable(x::CairoBackend, ::MIME"application/postscript", scene::Scene) = x.typ == EPS +Makie.backend_showable(x::CairoBackend, ::MIME"image/png", scene::Scene) = x.typ == PNG + + +function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"image/svg+xml", scene::Scene) + proxy_io = IOBuffer() + pt_per_unit = get(io, :pt_per_unit, x.pt_per_unit) + + screen = CairoScreen(scene, proxy_io, :svg; device_scaling_factor = pt_per_unit) + cairo_draw(screen, scene) + Cairo.flush(screen.surface) + Cairo.finish(screen.surface) + svg = String(take!(proxy_io)) + + # for some reason, in the svg, surfaceXXX ids keep counting up, + # even with the very same figure drawn again and again + # so we need to reset them to counting up from 1 + # so that the same figure results in the same svg and in the same salt + surfaceids = sort(unique(collect(m.match for m in eachmatch(r"surface\d+", svg)))) + + for (i, id) in enumerate(surfaceids) + svg = replace(svg, id => "surface$i") + end + + # salt svg glyphs with the first 8 characters of the base64 encoded + # sha512 hash to avoid collisions across svgs when embedding them on + # websites. the hash and therefore the salt will always be the same for the same file + # so the output is deterministic + salt = String(Base64.base64encode(SHA.sha512(svg)))[1:8] + svg_salted = replace(svg, r"glyph(?=\d+-\d+)" => salt) + print(io, svg_salted) + return screen +end + +function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"application/pdf", scene::Scene) + + pt_per_unit = get(io, :pt_per_unit, x.pt_per_unit) + + screen = CairoScreen(scene, io, :pdf; device_scaling_factor = pt_per_unit) + cairo_draw(screen, scene) + Cairo.finish(screen.surface) + return screen +end + + +function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"application/postscript", scene::Scene) + + pt_per_unit = get(io, :pt_per_unit, x.pt_per_unit) + + screen = CairoScreen(scene, io, :eps; device_scaling_factor = pt_per_unit) + + cairo_draw(screen, scene) + Cairo.finish(screen.surface) + return screen +end + +function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"image/png", scene::Scene) + + # multiply the resolution of the png with this factor for more or less detail + # while relative line and font sizes are unaffected + px_per_unit = get(io, :px_per_unit, x.px_per_unit) + # create an ARGB surface, to speed up drawing ops. + screen = CairoScreen(scene; device_scaling_factor = px_per_unit) + cairo_draw(screen, scene) + Cairo.write_to_png(screen.surface, io) + return screen +end + + +######################################## +# Fast colorbuffer for recording # +######################################## + +function Makie.colorbuffer(screen::CairoScreen) + # extract scene + scene = screen.scene + # get resolution + w, h = size(scene) + # preallocate an image matrix + img = Matrix{ARGB32}(undef, w, h) + # create an image surface to draw onto the image + surf = Cairo.CairoImageSurface(img) + # draw the scene onto the image matrix + ctx = Cairo.CairoContext(surf) + scr = CairoScreen(scene, surf, ctx, nothing) + + cairo_draw(scr, scene) + + # x and y are flipped - return the transpose + return permutedims(img) +end diff --git a/CairoMakie/src/overrides.jl b/CairoMakie/src/overrides.jl new file mode 100644 index 00000000000..8e7f6100fa4 --- /dev/null +++ b/CairoMakie/src/overrides.jl @@ -0,0 +1,171 @@ +################################################################################ +# Poly - the not so primitive, primitive # +################################################################################ + + +""" +Special method for polys so we don't fall back to atomic meshes, which are much more +complex and slower to draw than standard paths with single color. +""" +function draw_plot(scene::Scene, screen::CairoScreen, 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)...) +end + +""" +Fallback method for args without special treatment. +""" +function draw_poly(scene::Scene, screen::CairoScreen, poly, args...) + draw_poly_as_mesh(scene, screen, poly) +end + +function draw_poly_as_mesh(scene, screen, poly) + draw_plot(scene, screen, poly.plots[1]) + draw_plot(scene, screen, poly.plots[2]) +end + + +# in the rare case of per-vertex colors redirect to mesh drawing +function draw_poly(scene::Scene, screen::CairoScreen, poly, points::Vector{<:Point2}, color::AbstractArray, model, strokecolor, strokewidth) + draw_poly_as_mesh(scene, screen, poly) +end + +function draw_poly(scene::Scene, screen::CairoScreen, poly, points::Vector{<:Point2}) + draw_poly(scene, screen, poly, points, poly.color[], poly.model[], poly.strokecolor[], poly.strokewidth[]) +end + +function draw_poly(scene::Scene, screen::CairoScreen, poly, points::Vector{<:Point2}, color, model, strokecolor, strokewidth) + points = project_position.(Ref(scene), points, Ref(model)) + Cairo.move_to(screen.context, points[1]...) + for p in points[2:end] + Cairo.line_to(screen.context, p...) + end + Cairo.close_path(screen.context) + Cairo.set_source_rgba(screen.context, rgbatuple(to_color(color))...) + Cairo.fill_preserve(screen.context) + Cairo.set_source_rgba(screen.context, rgbatuple(to_color(strokecolor))...) + Cairo.set_line_width(screen.context, strokewidth) + Cairo.stroke(screen.context) +end + +function draw_poly(scene::Scene, screen::CairoScreen, poly, points_list::Vector{<:Vector{<:Point2}}) + broadcast_foreach(points_list, poly.color[], + poly.strokecolor[], poly.strokewidth[]) do points, color, strokecolor, strokewidth + + draw_poly(scene, screen, poly, points, color, poly.model[], strokecolor, strokewidth) + end +end + + +draw_poly(scene::Scene, screen::CairoScreen, poly, rect::Rect2D) = draw_poly(scene, screen, poly, [rect]) + +function draw_poly(scene::Scene, screen::CairoScreen, poly, rects::Vector{<:Rect2D}) + model = poly.model[] + projected_rects = project_rect.(Ref(scene), rects, Ref(model)) + + color = poly.color[] + if color isa AbstractArray{<:Number} + color = numbers_to_colors(color, poly) + elseif color isa String + # string is erroneously broadcasted as chars otherwise + color = to_color(color) + end + strokecolor = poly.strokecolor[] + if strokecolor isa AbstractArray{<:Number} + strokecolor = numbers_to_colors(strokecolor, poly) + elseif strokecolor isa String + # string is erroneously broadcasted as chars otherwise + strokecolor = to_color(strokecolor) + end + + broadcast_foreach(projected_rects, color, strokecolor, poly.strokewidth[]) do r, c, sc, sw + Cairo.rectangle(screen.context, origin(r)..., widths(r)...) + Cairo.set_source_rgba(screen.context, rgbatuple(to_color(c))...) + Cairo.fill_preserve(screen.context) + Cairo.set_source_rgba(screen.context, rgbatuple(to_color(sc))...) + Cairo.set_line_width(screen.context, sw) + Cairo.stroke(screen.context) + end +end + +function polypath(ctx, polygon) + ext = decompose(Point2f0, polygon.exterior) + Cairo.move_to(ctx, ext[1]...) + for point in ext[2:end] + Cairo.line_to(ctx, point...) + end + Cairo.close_path(ctx) + + interiors = decompose.(Point2f0, polygon.interiors) + for interior in interiors + Cairo.move_to(ctx, interior[1]...) + for point in interior[2:end] + Cairo.line_to(ctx, point...) + end + Cairo.close_path(ctx) + end +end + +function draw_poly(scene::Scene, screen::CairoScreen, poly, polygons::AbstractArray{<:Polygon}) + + model = poly.model[] + projected_polys = project_polygon.(Ref(scene), polygons, Ref(model)) + + color = poly.color[] + if color isa AbstractArray{<:Number} + color = numbers_to_colors(color, poly) + elseif color isa String + # string is erroneously broadcasted as chars otherwise + color = to_color(color) + end + strokecolor = poly.strokecolor[] + if strokecolor isa AbstractArray{<:Number} + strokecolor = numbers_to_colors(strokecolor, poly) + elseif strokecolor isa String + # string is erroneously broadcasted as chars otherwise + strokecolor = to_color(strokecolor) + end + + broadcast_foreach(projected_polys, color, strokecolor, poly.strokewidth[]) do po, c, sc, sw + polypath(screen.context, po) + Cairo.set_source_rgba(screen.context, rgbatuple(to_color(c))...) + Cairo.fill_preserve(screen.context) + Cairo.set_source_rgba(screen.context, rgbatuple(to_color(sc))...) + Cairo.set_line_width(screen.context, sw) + Cairo.stroke(screen.context) + end + +end + + +################################################################################ +# Band # +# Override because band is usually a polygon, but because it supports # +# gradients as well via `mesh` we have to intercept the poly use # +################################################################################ + +function draw_plot(scene::Scene, screen::CairoScreen, + band::Band{<:Tuple{<:AbstractVector{<:Point2},<:AbstractVector{<:Point2}}}) + + if !(band.color[] isa AbstractArray) + upperpoints = band[1][] + lowerpoints = band[2][] + points = vcat(lowerpoints, reverse(upperpoints)) + model = band.model[] + points = project_position.(Ref(scene), points, Ref(model)) + Cairo.move_to(screen.context, points[1]...) + for p in points[2:end] + Cairo.line_to(screen.context, p...) + end + Cairo.close_path(screen.context) + Cairo.set_source_rgba(screen.context, rgbatuple(to_color(band.color[]))...) + Cairo.fill(screen.context) + else + for p in band.plots + draw_plot(scene, screen, p) + end + end + + nothing +end \ No newline at end of file diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl new file mode 100644 index 00000000000..4bff8ac36d6 --- /dev/null +++ b/CairoMakie/src/primitives.jl @@ -0,0 +1,892 @@ +################################################################################ +# Lines, LineSegments # +################################################################################ + +function draw_atomic(scene::Scene, screen::CairoScreen, primitive::Union{Lines, LineSegments}) + fields = @get_attribute(primitive, (color, linewidth, linestyle)) + linestyle = Makie.convert_attribute(linestyle, Makie.key"linestyle"()) + ctx = screen.context + model = primitive[:model][] + positions = primitive[1][] + + isempty(positions) && return + + # workaround for a LineSegments object created from a GLNormalMesh + # the input argument is a view of points using faces, which results in + # a vector of tuples of two points. we convert those to a list of points + # so they don't trip up the rest of the pipeline + # TODO this shouldn't be necessary anymore! + if positions isa SubArray{<:Point3, 1, P, <:Tuple{Array{<:AbstractFace}}} where P + positions = let + pos = Point3f0[] + for tup in positions + push!(pos, tup[1]) + push!(pos, tup[2]) + end + pos + end + end + + projected_positions = project_position.(Ref(scene), positions, Ref(model)) + + if color isa AbstractArray{<: Number} + color = numbers_to_colors(color, primitive) + end + + # color is now a color or an array of colors + # if it's an array of colors, each segment must be stroked separately + + # The linestyle can be set globally, as we do here. + # However, there is a discrepancy between Makie + # and Cairo when it comes to linestyles. + # For Makie, the linestyle array is cumulative, + # and defines the "absolute" endpoints of segments. + # However, for Cairo, each value provides the length of + # alternate "on" and "off" portions of the stroke. + # Therefore, we take the diff of the given linestyle, + # to convert the "absolute" coordinates into "relative" ones. + if !isnothing(linestyle) && !(linewidth isa AbstractArray) + Cairo.set_dash(ctx, diff(Float64.(linestyle)) .* linewidth) + end + if color isa AbstractArray || linewidth isa AbstractArray + # stroke each segment separately, this means disjointed segments with probably + # wonky dash patterns if segments are short + + # we can hide the gaps by setting the line cap to round + Cairo.set_line_cap(ctx, Cairo.CAIRO_LINE_CAP_ROUND) + draw_multi( + primitive, ctx, + projected_positions, + color, linewidth, + isnothing(linestyle) ? nothing : diff(Float64.(linestyle)) + ) + else + # stroke the whole line at once if it has only one color + # this allows correct linestyles and line joins as well and will be the + # most common case + Cairo.set_line_width(ctx, linewidth) + Cairo.set_source_rgba(ctx, red(color), green(color), blue(color), alpha(color)) + draw_single(primitive, ctx, projected_positions) + end + nothing +end + +function draw_single(primitive::Lines, ctx, positions) + n = length(positions) + @inbounds for i in 1:n + p = positions[i] + # only take action for non-NaNs + if !isnan(p) + # new line segment at beginning or if previously NaN + if i == 1 || isnan(positions[i-1]) + Cairo.move_to(ctx, p...) + else + Cairo.line_to(ctx, p...) + # complete line segment at end or if next point is NaN + if i == n || isnan(positions[i+1]) + Cairo.stroke(ctx) + end + end + end + end +end + +function draw_single(primitive::LineSegments, ctx, positions) + + @assert iseven(length(positions)) + + @inbounds for i in 1:2:length(positions)-1 + p1 = positions[i] + p2 = positions[i+1] + + if isnan(p1) || isnan(p2) + continue + else + Cairo.move_to(ctx, p1...) + Cairo.line_to(ctx, p2...) + Cairo.stroke(ctx) + end + end +end + +# if linewidth is not an array +function draw_multi(primitive, ctx, positions, colors::AbstractArray, linewidth, dash) + draw_multi(primitive, ctx, positions, colors, [linewidth for c in colors], dash) +end + +# if color is not an array +function draw_multi(primitive, ctx, positions, color, linewidths::AbstractArray, dash) + draw_multi(primitive, ctx, positions, [color for l in linewidths], linewidths, dash) +end + +function draw_multi(primitive::Union{Lines, LineSegments}, ctx, positions, colors::AbstractArray, linewidths::AbstractArray, dash) + if primitive isa LineSegments + @assert iseven(length(positions)) + end + @assert length(positions) == length(colors) + @assert length(linewidths) == length(colors) + + iterator = if primitive isa Lines + 1:length(positions)-1 + elseif primitive isa LineSegments + 1:2:length(positions) + end + + for i in iterator + if isnan(positions[i+1]) || isnan(positions[i]) + continue + end + Cairo.move_to(ctx, positions[i]...) + + Cairo.line_to(ctx, positions[i+1]...) + if linewidths[i] != linewidths[i+1] + error("Cairo doesn't support two different line widths ($(linewidths[i]) and $(linewidths[i+1])) at the endpoints of a line.") + end + Cairo.set_line_width(ctx, linewidths[i]) + !isnothing(dash) && Cairo.set_dash(ctx, dash .* linewidths[i]) + c1 = colors[i] + c2 = colors[i+1] + # we can avoid the more expensive gradient if the colors are the same + # this happens if one color was given for each segment + if c1 == c2 + Cairo.set_source_rgba(ctx, red(c1), green(c1), blue(c1), alpha(c1)) + Cairo.stroke(ctx) + else + pat = Cairo.pattern_create_linear(positions[i]..., positions[i+1]...) + Cairo.pattern_add_color_stop_rgba(pat, 0, red(c1), green(c1), blue(c1), alpha(c1)) + Cairo.pattern_add_color_stop_rgba(pat, 1, red(c2), green(c2), blue(c2), alpha(c2)) + Cairo.set_source(ctx, pat) + Cairo.stroke(ctx) + Cairo.destroy(pat) + end + end +end + +################################################################################ +# Scatter # +################################################################################ + +function draw_atomic(scene::Scene, screen::CairoScreen, primitive::Scatter) + fields = @get_attribute(primitive, (color, markersize, strokecolor, strokewidth, marker, marker_offset, rotations)) + @get_attribute(primitive, (transform_marker,)) + + ctx = screen.context + model = primitive[:model][] + positions = primitive[1][] + isempty(positions) && return + size_model = transform_marker ? model : Mat4f0(I) + + font = to_font(to_value(get(primitive, :font, Makie.defaultfont()))) + + colors = if color isa AbstractArray{<: Number} + numbers_to_colors(color, primitive) + else + color + end + + broadcast_foreach(primitive[1][], colors, markersize, strokecolor, + strokewidth, marker, marker_offset, remove_billboard(rotations)) do point, col, + markersize, strokecolor, strokewidth, marker, mo, rotation + + # if we give size in pixels, the size is always equal to that value + is_pixelspace = haskey(primitive, :markerspace) && primitive.markerspace[] == Makie.Pixel + scale = if is_pixelspace + Makie.to_2d_scale(markersize) + else + # otherwise calculate a scaled size + project_scale(scene, markersize, size_model) + end + offset = if is_pixelspace + Makie.to_2d_scale(mo) + else + project_scale(scene, mo, size_model) + end + + pos = project_position(scene, point, model) + + isnan(pos) && return + + Cairo.set_source_rgba(ctx, rgbatuple(col)...) + m = convert_attribute(marker, key"marker"(), key"scatter"()) + if m isa Char + draw_marker(ctx, m, best_font(m, font), pos, scale, strokecolor, strokewidth, offset, rotation) + else + draw_marker(ctx, m, pos, scale, strokecolor, strokewidth, offset, rotation) + end + end + nothing +end + + +function draw_marker(ctx, marker::Circle, pos, scale, strokecolor, strokewidth, marker_offset, rotation) + + marker_offset = marker_offset + scale ./ 2 + + pos += Point2f0(marker_offset[1], -marker_offset[2]) + + # Cairo.scale(ctx, scale...) + Cairo.move_to(ctx, pos[1] + scale[1]/2, pos[2]) + Cairo.arc(ctx, pos[1], pos[2], scale[1]/2, 0, 2*pi) + Cairo.fill_preserve(ctx) + + sc = to_color(strokecolor) + + Cairo.set_source_rgba(ctx, rgbatuple(sc)...) + Cairo.set_line_width(ctx, Float64(strokewidth)) + Cairo.stroke(ctx) +end + +function draw_marker(ctx, marker::Char, font, pos, scale, strokecolor, strokewidth, marker_offset, rotation) + + Cairo.save(ctx) + + # Marker offset is meant to be relative to the + # bottom left corner of the box centered at + # `pos` with sides defined by `scale`, but + # this does not take the character's dimensions + # into account. + # Here, we reposition the marker offset to be + # relative to the center of the char. + marker_offset = marker_offset .+ scale ./ 2 + + cairoface = set_ft_font(ctx, font) + + charextent = Makie.FreeTypeAbstraction.internal_get_extent(font, marker) + inkbb = Makie.FreeTypeAbstraction.inkboundingbox(charextent) + + # scale normalized bbox by font size + inkbb_scaled = FRect2D(origin(inkbb) .* scale, widths(inkbb) .* scale) + + # flip y for the centering shift of the character because in Cairo y goes down + centering_offset = [1, -1] .* (-origin(inkbb_scaled) .- 0.5 .* widths(inkbb_scaled)) + # this is the origin where we actually have to place the glyph so it can be centered + charorigin = pos .+ Vec2f0(marker_offset[1], -marker_offset[2]) + old_matrix = get_font_matrix(ctx) + set_font_matrix(ctx, scale_matrix(scale...)) + + # First, we translate to the point where the + # marker is supposed to go. + Cairo.translate(ctx, charorigin...) + # Then, we rotate the context by the + # appropriate amount, + Cairo.rotate(ctx, to_2d_rotation(rotation)) + # and apply a centering offset to account for + # the fact that text is shown from the (relative) + # bottom left corner. + Cairo.translate(ctx, centering_offset...) + + Cairo.move_to(ctx, 0, 0) + Cairo.text_path(ctx, string(marker)) + Cairo.fill_preserve(ctx) + # stroke + Cairo.set_line_width(ctx, strokewidth) + Cairo.set_source_rgba(ctx, rgbatuple(strokecolor)...) + Cairo.stroke(ctx) + + # if we use set_ft_font we should destroy the pointer it returns + cairo_font_face_destroy(cairoface) + + set_font_matrix(ctx, old_matrix) + Cairo.restore(ctx) + +end + + +function draw_marker(ctx, marker::Union{Rect, Type{<: Rect}}, pos, scale, strokecolor, strokewidth, marker_offset, rotation) + s2 = if marker isa Type{Rect} + Point2(scale[1], -scale[2]) + else + Point2((widths(marker) .* scale .* (1, -1))...) + end + + offset = marker_offset .+ scale ./ 2 + + pos += Point2f0(offset[1], -offset[2]) + + Cairo.move_to(ctx, pos...) + Cairo.rotate(ctx, to_2d_rotation(rotation)) + Cairo.rectangle(ctx, 0, 0, s2...) + Cairo.fill_preserve(ctx); + if strokewidth > 0.0 + sc = to_color(strokecolor) + Cairo.set_source_rgba(ctx, rgbatuple(sc)...) + Cairo.set_line_width(ctx, Float64(strokewidth)) + Cairo.stroke(ctx) + end +end + + +################################################################################ +# Text # +################################################################################ + +function p3_to_p2(p::Point3{T}) where T + if p[3] == 0 || isnan(p[3]) + Point2{T}(p[1:2]...) + else + error("Can't reduce Point3 to Point2 with nonzero third component $(p[3]).") + end +end + +function draw_atomic(scene::Scene, screen::CairoScreen, primitive::Text) + ctx = screen.context + @get_attribute(primitive, (textsize, color, font, rotation, model, space, offset)) + txt = to_value(primitive[1]) + position = primitive.attributes[:position][] + # use cached glyph info + glyphlayouts = primitive._glyphlayout[] + + draw_string(scene, ctx, txt, position, glyphlayouts, textsize, color, font, + remove_billboard(rotation), model, space, offset) + + nothing +end + +function draw_string(scene, ctx, strings::AbstractArray, positions::AbstractArray, glyphlayouts, textsize, color, font, rotation, model::SMatrix, space, offset) + + # TODO: why is the Ref around model necessary? doesn't broadcast_foreach handle staticarrays matrices? + broadcast_foreach(strings, positions, glyphlayouts, textsize, color, font, rotation, + Ref(model), space, offset) do str, pos, glayout, ts, c, f, ro, mo, sp, off + + draw_string(scene, ctx, str, pos, glayout, ts, c, f, ro, mo, sp, off) + end +end + +function draw_string(scene, ctx, str::String, position::VecTypes, glyphlayout, textsize, color, font, rotation, model, space, offset) + + glyphoffsets = glyphlayout.origins + + Cairo.save(ctx) + cairoface = set_ft_font(ctx, font) + Cairo.set_source_rgba(ctx, rgbatuple(color)...) + old_matrix = get_font_matrix(ctx) + + + for (i, char) in enumerate(str) + goffset = glyphoffsets[i] + if offset isa Vector + p3_offset = to_ndim(Point3f0, offset[i], 0) + else + p3_offset = to_ndim(Point3f0, offset, 0) + end + ts = textsize isa Vector ? textsize[i] : textsize + + + char in ('\r', '\n') && continue + + if space == :data + # in data space, the glyph offsets are just added to the string positions + # and then projected + + # glyph position in data coordinates (offset has rotation applied already) + gpos_data = to_ndim(Point3f0, position, 0) .+ goffset .+ p3_offset + + scale3 = ts isa Number ? Point3f0(ts, ts, 0) : to_ndim(Point3f0, ts, 0) + + # this could be done better but it works at least + + # the CairoMatrix is found by transforming the right and up vector + # of the character into screen space and then subtracting the projected + # origin. The resulting vectors give the directions in which the character + # needs to be stretched in order to match the 3D projection + + xvec = rotation * (scale3[1] * Point3f0(1, 0, 0)) + yvec = rotation * (scale3[2] * Point3f0(0, -1, 0)) + + gproj = project_position(scene, gpos_data, Mat4f0(I)) + xproj = project_position(scene, gpos_data + xvec, Mat4f0(I)) + yproj = project_position(scene, gpos_data + yvec, Mat4f0(I)) + + xdiff = xproj - gproj + ydiff = yproj - gproj + + mat = Cairo.CairoMatrix( + xdiff[1], xdiff[2], + ydiff[1], ydiff[2], + 0, 0, + ) + + Cairo.save(ctx) + Cairo.move_to(ctx, gproj...) + set_font_matrix(ctx, mat) + + # Cairo.rotate(ctx, to_2d_rotation(rotation)) + Cairo.show_text(ctx, string(char)) + Cairo.restore(ctx) + + elseif space == :screen + # in screen space, the glyph offsets are added after projecting + # the string position into screen space + glyphpos = project_position( + scene, + position, + Mat4f0(I)) .+ (p3_to_p2(goffset .+ p3_offset)) .* (1, -1) # flip for Cairo + # and the scale is just taken as is + scale = length(ts) == 2 ? ts : SVector(ts, ts) + + Cairo.save(ctx) + Cairo.move_to(ctx, glyphpos...) + # TODO this only works in 2d + mat = scale_matrix(scale...) + set_font_matrix(ctx, mat) + Cairo.rotate(ctx, to_2d_rotation(rotation)) + + Cairo.show_text(ctx, string(char)) + Cairo.restore(ctx) + else + error() + end + end + + cairo_font_face_destroy(cairoface) + set_font_matrix(ctx, old_matrix) + Cairo.restore(ctx) + + nothing +end + +################################################################################ +# Heatmap, Image # +################################################################################ + +""" + regularly_spaced_array_to_range(arr) +If possible, converts `arr` to a range. +If not, returns array unchanged. +""" +function regularly_spaced_array_to_range(arr) + diffs = unique!(sort!(diff(arr))) + step = sum(diffs) ./ length(diffs) + if all(x-> x ≈ step, diffs) + m, M = extrema(arr) + if step < zero(step) + m, M = M, m + end + # don't use stop=M, since that may not include M + return range(m; step, length=length(arr)) + else + return arr + end +end + +""" + interpolation_flag(is_vector, interp, wpx, hpx, w, h) + +* is_vector: if we're using vector backend +* interp: does the user want to interpolate? +* wpx, hpx: projected size of the image in pixels, so the actual width in pixels on screen +* w, h: size of image in pixels +""" +function interpolation_flag(is_vector, interp, wpx, hpx, w, h) + if interp + if is_vector + @warn("Using billinear filtering for vector backends, which can result in downsampling artifacts") + return Cairo.FILTER_BILINEAR + else + return Cairo.FILTER_BEST + end + else + if wpx < w || hpx < h + # if size of image size in pixels is larger then the rectangle it gets drawn into, + # the pixels will be smaller than what ends up on screen, so one won't be able to see rectangles. + # In that case, we need to apply filtering, or we get artifacts from incorrectly downsampling! + return interpolation_flag(is_vector, true, wpx, hpx, w, h) + else + return Cairo.FILTER_NEAREST + end + end +end + + +function draw_atomic(scene::Scene, screen::CairoScreen, primitive::Union{Heatmap, Image}) + ctx = screen.context + image = primitive[3][] + xs, ys = primitive[1][], primitive[2][] + + if !(xs isa Vector) + l, r = extrema(xs) + N = size(image, 1) + xs = range(l, r, length = N+1) + else + xs = regularly_spaced_array_to_range(xs) + end + if !(ys isa Vector) + l, r = extrema(ys) + N = size(image, 2) + ys = range(l, r, length = N+1) + else + ys = regularly_spaced_array_to_range(ys) + end + model = primitive[:model][] + imsize = (extrema_nan(xs), extrema_nan(ys)) + + interp = to_value(get(primitive, :interpolate, true)) + weird_cairo_limit = (2^15) - 23 + + # Debug attribute we can set to disable fastpath + # probably shouldn't really be part of the interface + fast_path = to_value(get(primitive, :fast_path, true)) + # Vector backends don't support FILTER_NEAREST for interp == false, so in that case we also need to draw rects + is_vector = is_vector_backend(ctx) + if fast_path && xs isa AbstractRange && ys isa AbstractRange && !(is_vector && !interp) + # find projected image corners + # this already takes care of flipping the image to correct cairo orientation + xy = project_position(scene, Point2f0(first.(imsize)), model) + xymax = project_position(scene, Point2f0(last.(imsize)), model) + w, h = xymax .- xy + + s = to_cairo_image(image, primitive) + + interp_flag = interpolation_flag(is_vector, interp, abs(w), abs(h), s.width, s.height) + + if s.width > weird_cairo_limit || s.height > weird_cairo_limit + error("Cairo stops rendering images bigger than $(weird_cairo_limit), which is likely a bug in Cairo. Please resample your image/heatmap with e.g. `ImageTransformations.imresize`") + end + Cairo.rectangle(ctx, xy..., w, h) + Cairo.save(ctx) + Cairo.translate(ctx, xy...) + Cairo.scale(ctx, w / s.width, h / s.height) + Cairo.set_source_surface(ctx, s, 0, 0) + p = Cairo.get_source(ctx) + # this is needed to avoid blurry edges + Cairo.pattern_set_extend(p, Cairo.EXTEND_PAD) + # Set filter doesn't work!? + Cairo.pattern_set_filter(p, interp_flag) + Cairo.fill(ctx) + Cairo.restore(ctx) + + else + # We need to draw rectangles for vector backends, or irregular grids + if interp + error("Interpolation for non gridded heatmaps/images isn't supported right now. Please use interpolate=false for this plot") + end + # find projected image corners + # this already takes care of flipping the image to correct cairo orientation + xys = [project_position(scene, Point2f0(x, y), model) for x in xs, y in ys] + colors = to_rgba_image(image, primitive) + + # Note: xs and ys should have size ni+1, nj+1 + ni, nj = size(image) + if ni + 1 != length(xs) || nj + 1 != length(ys) + error("Error in conversion pipeline. xs and ys should have size ni+1, nj+1. Found: xs: $(length(xs)), ys: $(length(ys)), ni: $(ni), nj: $(nj)") + end + @inbounds for i in 1:ni, j in 1:nj + x0, y0 = xys[i, j] + x1, y1 = xys[i+1, j+1] + w = x1 - x0; h = y1 - y0 + + # there are usually white lines between directly adjacent rectangles + # in vector graphics because of anti-aliasing + + # if we let each cell stick out (bulge) a little bit (half a point) under its neighbors + # those lines disappear + + # we heuristically only do this if the adjacent cells are fully opaque + # and if we're not in the last row / column so the overall heatmap doesn't get bigger + + # this should be the most common case by far, though + + xbulge = if i < ni && alpha(colors[i+1, j]) == 1 + 0.5 + else + 0.0 + end + ybulge = if j < nj && alpha(colors[i, j+1]) == 1 + 0.5 + else + 0.0 + end + + # we add the bulge in the direction of cell width / height in case the axes are reversed + Cairo.rectangle(ctx, x0, y0, w + sign(w) * xbulge, h + sign(h) * ybulge) + Cairo.set_source_rgba(ctx, rgbatuple(colors[i, j])...) + Cairo.fill(ctx) + end + end +end + + +################################################################################ +# Mesh # +################################################################################ + + +function draw_atomic(scene::Scene, screen::CairoScreen, primitive::Makie.Mesh) + if scene.camera_controls[] isa Union{Camera2D, Makie.PixelCamera} + draw_mesh2D(scene, screen, primitive) + else + if !haskey(primitive, :faceculling) + primitive[:faceculling] = Node(-10) + end + draw_mesh3D(scene, screen, primitive) + end + return nothing +end + +function draw_mesh2D(scene, screen, primitive) + @get_attribute(primitive, (color,)) + + colormap = get(primitive, :colormap, nothing) |> to_value |> to_colormap + colorrange = get(primitive, :colorrange, nothing) |> to_value + ctx = screen.context + model = primitive.model[] + mesh = GeometryBasics.mesh(primitive[1][]) + # Priorize colors of the mesh if present + # This is a hack, which needs cleaning up in the Mesh plot type! + color = hasproperty(mesh, :color) ? mesh.color : color + vs = decompose(Point, mesh); fs = decompose(TriangleFace, mesh) + uv = hasproperty(mesh, :uv) ? mesh.uv : nothing + pattern = Cairo.CairoPatternMesh() + + cols = per_face_colors(color, colormap, colorrange, nothing, vs, fs, nothing, uv) + for (f, (c1, c2, c3)) in zip(fs, cols) + t1, t2, t3 = project_position.(scene, vs[f], (model,)) #triangle points + Cairo.mesh_pattern_begin_patch(pattern) + + Cairo.mesh_pattern_move_to(pattern, t1...) + Cairo.mesh_pattern_line_to(pattern, t2...) + Cairo.mesh_pattern_line_to(pattern, t3...) + + mesh_pattern_set_corner_color(pattern, 0, c1) + mesh_pattern_set_corner_color(pattern, 1, c2) + mesh_pattern_set_corner_color(pattern, 2, c3) + + Cairo.mesh_pattern_end_patch(pattern) + end + Cairo.set_source(ctx, pattern) + Cairo.close_path(ctx) + Cairo.paint(ctx) + return nothing +end + +function average_z(positions, face) + vs = positions[face] + sum(v -> v[3], vs) / length(vs) +end + +nan2zero(x) = !isnan(x) * x + +function draw_mesh3D( + scene, screen, primitive; + mesh = primitive[1][], pos = Vec4f0(0), scale = 1f0 + ) + @get_attribute(primitive, (color, shading, lightposition, ambient, diffuse, + specular, shininess, faceculling)) + + colormap = get(primitive, :colormap, nothing) |> to_value |> to_colormap + colorrange = get(primitive, :colorrange, nothing) |> to_value + matcap = get(primitive, :matcap, nothing) |> to_value + # Priorize colors of the mesh if present + color = hasproperty(mesh, :color) ? mesh.color : color + + ctx = screen.context + + model = primitive.model[] + view = scene.camera.view[] + projection = scene.camera.projection[] + normalmatrix = get( + scene.attributes, :normalmatrix, let + i = SOneTo(3) + transpose(inv(view[i, i] * model[i, i])) + end + ) + + # Mesh data + # transform to view/camera space + vs = map(decompose(Point, mesh)) do v + # Should v get a nan2zero? + p4d = to_ndim(Vec4f0, scale .* to_ndim(Vec3f0, v, 0f0), 1f0) + view * (model * p4d .+ to_ndim(Vec4f0, pos, 0f0)) + end + fs = decompose(GLTriangleFace, mesh) + uv = hasproperty(mesh, :uv) ? mesh.uv : nothing + ns = map(n -> normalize(normalmatrix * n), normals(mesh)) + cols = per_face_colors( + color, colormap, colorrange, matcap, vs, fs, ns, uv, + get(primitive, :lowclip, nothing) |> to_value |> color_or_nothing, + get(primitive, :highclip, nothing) |> to_value |> color_or_nothing, + get(primitive, :nan_color, nothing) |> to_value |> color_or_nothing + ) + + # Liight math happens in view/camera space + if lightposition == :eyeposition + lightposition = scene.camera.eyeposition[] + end + lightpos = (view * to_ndim(Vec4f0, lightposition, 1.0))[Vec(1, 2, 3)] + + # Camera to screen space + ts = map(vs) do v + clip = projection * v + @inbounds begin + p = (clip ./ clip[4])[Vec(1, 2)] + p_yflip = Vec2f0(p[1], -p[2]) + p_0_to_1 = (p_yflip .+ 1f0) / 2f0 + end + p = p_0_to_1 .* scene.camera.resolution[] + Vec3f0(p[1], p[2], clip[3]) + end + + # Approximate zorder + zorder = sortperm(fs, by = f -> average_z(ts, f)) + + # Face culling + zorder = filter(i -> any(last.(ns[fs[i]]) .> faceculling), zorder) + + pattern = Cairo.CairoPatternMesh() + for k in reverse(zorder) + f = fs[k] + t1, t2, t3 = ts[f] + + # light calculation + c1, c2, c3 = if shading + map(ns[f], vs[f], cols[k]) do N, v, c + L = normalize(lightpos .- v[Vec(1,2,3)]) + diff_coeff = max(dot(L, N), 0.0) + H = normalize(L + normalize(-v[SOneTo(3)])) + spec_coeff = max(dot(H, N), 0.0)^shininess + c = RGBA(c) + new_c = (ambient .+ diff_coeff .* diffuse) .* Vec3f0(c.r, c.g, c.b) .+ + specular * spec_coeff + RGBA(new_c..., c.alpha) + end + else + cols[k] + end + # debug normal coloring + # n1, n2, n3 = Vec3f0(0.5) .+ 0.5ns[f] + # c1 = RGB(n1...) + # c2 = RGB(n2...) + # c3 = RGB(n3...) + + Cairo.mesh_pattern_begin_patch(pattern) + + Cairo.mesh_pattern_move_to(pattern, t1[1], t1[2]) + Cairo.mesh_pattern_line_to(pattern, t2[1], t2[2]) + Cairo.mesh_pattern_line_to(pattern, t3[1], t3[2]) + + mesh_pattern_set_corner_color(pattern, 0, c1) + mesh_pattern_set_corner_color(pattern, 1, c2) + mesh_pattern_set_corner_color(pattern, 2, c3) + + Cairo.mesh_pattern_end_patch(pattern) + end + Cairo.set_source(ctx, pattern) + Cairo.close_path(ctx) + Cairo.paint(ctx) + return nothing +end + + +################################################################################ +# Surface # +################################################################################ + + +function draw_atomic(scene::Scene, screen::CairoScreen, primitive::Makie.Surface) + # Pretend the surface plot is a mesh plot and plot that instead + mesh = surface2mesh(primitive[1][], primitive[2][], primitive[3][]) + old = primitive[:color] + if old[] === nothing + primitive[:color] = primitive[3] + end + if !haskey(primitive, :faceculling) + primitive[:faceculling] = Node(-10) + end + draw_mesh3D(scene, screen, primitive, mesh=mesh) + primitive[:color] = old + return nothing +end + +function surface2mesh(xs::Makie.ClosedInterval, ys, zs::AbstractMatrix) + surface2mesh(range(xs.left, xs.right, length = size(zs, 1)), ys, zs) +end + +function surface2mesh(xs, ys::Makie.ClosedInterval, zs::AbstractMatrix) + surface2mesh(xs, range(ys.left, ys.right, length = size(zs, 2)), zs) +end + +function surface2mesh(xs::Makie.ClosedInterval, ys::Makie.ClosedInterval, zs::AbstractMatrix) + surface2mesh( + range(xs.left, xs.right, length = size(zs, 1)), + range(ys.left, ys.right, length = size(zs, 2)), + zs) +end + +function surface2mesh(xs::AbstractVector, ys::AbstractVector, zs::AbstractMatrix) + ps = [nan2zero.(Point3f0(xs[i], ys[j], zs[i, j])) for j in eachindex(ys) for i in eachindex(xs)] + idxs = LinearIndices(size(zs)) + faces = [ + QuadFace(idxs[i, j], idxs[i+1, j], idxs[i+1, j+1], idxs[i, j+1]) + for j in 1:size(zs, 2)-1 for i in 1:size(zs, 1)-1 + ] + normal_mesh(ps, faces) +end + +function surface2mesh(xs::AbstractMatrix, ys::AbstractMatrix, zs::AbstractMatrix) + ps = [nan2zero.(Point3f0(xs[i, j], ys[i, j], zs[i, j])) for j in 1:size(zs, 2) for i in 1:size(zs, 1)] + idxs = LinearIndices(size(zs)) + faces = [ + QuadFace(idxs[i, j], idxs[i+1, j], idxs[i+1, j+1], idxs[i, j+1]) + for j in 1:size(zs, 2)-1 for i in 1:size(zs, 1)-1 + ] + normal_mesh(ps, faces) +end + +################################################################################ +# MeshScatter # +################################################################################ + + +function draw_atomic(scene::Scene, screen::CairoScreen, primitive::Makie.MeshScatter) + @get_attribute(primitive, (color, model, marker, markersize, rotations)) + + if color isa AbstractArray{<: Number} + color = numbers_to_colors(color, primitive) + end + + m = convert_attribute(marker, key"marker"(), key"meshscatter"()) + pos = primitive[1][] + # For correct z-ordering we need to be in view/camera or screen space + model = copy(model) + view = scene.camera.view[] + + zorder = sortperm(pos, by = p -> begin + p4d = to_ndim(Vec4f0, to_ndim(Vec3f0, p, 0f0), 1f0) + cam_pos = view * model * p4d + cam_pos[3] / cam_pos[4] + end, rev=false) + + submesh = Attributes( + model=model, + color=color, + shading=primitive.shading, lightposition=primitive.lightposition, + ambient=primitive.ambient, diffuse=primitive.diffuse, + specular=primitive.specular, shininess=primitive.shininess, + faceculling=get(primitive, :faceculling, -10) + ) + + if !(rotations isa Vector) + R = Makie.rotationmatrix4(to_rotation(rotations)) + submesh[:model] = model * R + end + scales = primitive[:markersize][] + + for i in zorder + p = pos[i] + if color isa AbstractVector + submesh[:color] = 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 + + draw_mesh3D( + scene, screen, submesh, mesh = m, pos = p, + scale = scale isa Real ? Vec3f0(scale) : to_ndim(Vec3f0, scale, 1f0) + ) + end + + return nothing +end diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl new file mode 100644 index 00000000000..c6669d5209a --- /dev/null +++ b/CairoMakie/src/utils.jl @@ -0,0 +1,242 @@ +################################################################################ +# Projection utilities # +################################################################################ + +function project_position(scene, point, model) + + # use transform func + point = Makie.apply_transform(scene.transformation.transform_func[], point) + + res = scene.camera.resolution[] + p4d = to_ndim(Vec4f0, to_ndim(Vec3f0, point, 0f0), 1f0) + clip = scene.camera.projectionview[] * model * p4d + @inbounds begin + # between -1 and 1 + p = (clip ./ clip[4])[Vec(1, 2)] + # flip y to match cairo + p_yflip = Vec2f0(p[1], -p[2]) + # normalize to between 0 and 1 + p_0_to_1 = (p_yflip .+ 1f0) / 2f0 + end + # multiply with scene resolution for final position + return p_0_to_1 .* res +end + +project_scale(scene::Scene, s::Number, model = Mat4f0(I)) = project_scale(scene, Vec2f0(s), model) + +function project_scale(scene::Scene, s, model = Mat4f0(I)) + p4d = to_ndim(Vec4f0, s, 0f0) + p = @inbounds (scene.camera.projectionview[] * model * p4d)[Vec(1, 2)] ./ 2f0 + return p .* scene.camera.resolution[] +end + +function project_rect(scene, rect::Rect, model) + mini = project_position(scene, minimum(rect), model) + maxi = project_position(scene, maximum(rect), model) + return Rect(mini, maxi .- mini) +end + +function project_polygon(scene, poly::P, model) where P <: Polygon + ext = decompose(Point2f0, poly.exterior) + interiors = decompose.(Point2f0, poly.interiors) + Polygon( + Point2f0.(project_position.(Ref(scene), ext, Ref(model))), + [Point2f0.(project_position.(Ref(scene), interior, Ref(model))) for interior in interiors], + ) +end + +scale_matrix(x, y) = Cairo.CairoMatrix(x, 0.0, 0.0, y, 0.0, 0.0) + +######################################## +# Rotation handling # +######################################## + +function to_2d_rotation(x) + quat = to_rotation(x) + return -Makie.quaternion_to_2d_angle(quat) +end + +function to_2d_rotation(::Makie.Billboard) + @warn "This should not be reachable!" + 0 +end + +remove_billboard(x) = x +remove_billboard(b::Makie.Billboard) = b.rotation + +to_2d_rotation(quat::Makie.Quaternion) = -Makie.quaternion_to_2d_angle(quat) + +# TODO: this is a hack around a hack. +# Makie encodes the transformation from a 2-vector +# to a quaternion as a rotation around the Y-axis, +# when it should be a rotation around the X-axis. +# Since I don't know how to fix this in GLMakie, +# I've reversed the order of arguments to atan, +# such that our behaviour is consistent with GLMakie's. +to_2d_rotation(vec::Vec2f0) = atan(vec[1], vec[2]) + +to_2d_rotation(n::Real) = n + + +################################################################################ +# Color handling # +################################################################################ + +function rgbatuple(c::Colorant) + rgba = RGBA(c) + red(rgba), green(rgba), blue(rgba), alpha(rgba) +end + +function rgbatuple(c) + colorant = to_color(c) + if !(colorant isa Colorant) + error("Can't convert $(c) to a colorant") + end + return rgbatuple(colorant) +end + +to_uint32_color(c) = reinterpret(UInt32, convert(ARGB32, c)) + +function numbers_to_colors(numbers::AbstractArray{<:Number}, primitive) + + colormap = get(primitive, :colormap, nothing) |> to_value |> to_colormap + colorrange = get(primitive, :colorrange, nothing) |> to_value + + if colorrange === Makie.automatic + colorrange = extrema(numbers) + end + + Makie.interpolated_getindex.( + Ref(colormap), + Float64.(numbers), # ints don't work in interpolated_getindex + Ref(colorrange)) +end + +######################################## +# Image/heatmap -> ARGBSurface # +######################################## + +function to_cairo_image(img::AbstractMatrix{<: AbstractFloat}, attributes) + to_cairo_image(to_rgba_image(img, attributes), attributes) +end + +function to_rgba_image(img::AbstractMatrix{<: AbstractFloat}, attributes) + Makie.@get_attribute attributes (colormap, colorrange, nan_color, lowclip, highclip) + + nan_color = Makie.to_color(nan_color) + lowclip = isnothing(lowclip) ? lowclip : Makie.to_color(lowclip) + highclip = isnothing(highclip) ? highclip : Makie.to_color(highclip) + + [get_rgba_pixel(pixel, colormap, colorrange, nan_color, lowclip, highclip) for pixel in img] +end + +to_rgba_image(img::AbstractMatrix{<: Colorant}, attributes) = RGBAf0.(img) + +function get_rgba_pixel(pixel, colormap, colorrange, nan_color, lowclip, highclip) + vmin, vmax = colorrange + + if isnan(pixel) + RGBAf0(nan_color) + elseif pixel < vmin && !isnothing(lowclip) + RGBAf0(lowclip) + elseif pixel > vmax && !isnothing(highclip) + RGBAf0(highclip) + else + RGBAf0(Makie.interpolated_getindex(colormap, pixel, colorrange)) + end +end + +function to_cairo_image(img::AbstractMatrix{<: Colorant}, attributes) + to_cairo_image(to_uint32_color.(img), attributes) +end + +function to_cairo_image(img::Matrix{UInt32}, attributes) + # we need to convert from column-major to row-major storage, + # therefore we permute x and y + return Cairo.CairoARGBSurface(permutedims(img)) +end + + +################################################################################ +# Mesh handling # +################################################################################ + +struct FaceIterator{Iteration, T, F, ET} <: AbstractVector{ET} + data::T + faces::F +end + +function (::Type{FaceIterator{Typ}})(data::T, faces::F) where {Typ, T, F} + FaceIterator{Typ, T, F}(data, faces) +end +function (::Type{FaceIterator{Typ, T, F}})(data::AbstractVector, faces::F) where {Typ, F, T} + FaceIterator{Typ, T, F, NTuple{3, eltype(data)}}(data, faces) +end +function (::Type{FaceIterator{Typ, T, F}})(data::T, faces::F) where {Typ, T, F} + FaceIterator{Typ, T, F, NTuple{3, T}}(data, faces) +end +function FaceIterator(data::AbstractVector, faces) + if length(data) == length(faces) + FaceIterator{:PerFace}(data, faces) + else + FaceIterator{:PerVert}(data, faces) + end +end + + +Base.size(fi::FaceIterator) = size(fi.faces) +Base.getindex(fi::FaceIterator{:PerFace}, i::Integer) = fi.data[i] +Base.getindex(fi::FaceIterator{:PerVert}, i::Integer) = fi.data[fi.faces[i]] +Base.getindex(fi::FaceIterator{:Const}, i::Integer) = ntuple(i-> fi.data, 3) + +color_or_nothing(c) = c === nothing ? nothing : to_color(c) + +function per_face_colors( + color, colormap, colorrange, matcap, vertices, faces, normals, uv, + lowclip=nothing, highclip=nothing, nan_color=nothing + ) + if matcap !== nothing + wsize = reverse(size(matcap)) + wh = wsize .- 1 + cvec = map(normals) do n + muv = 0.5n[Vec(1,2)] .+ Vec2f0(0.5) + x, y = clamp.(round.(Int, Tuple(muv) .* wh) .+ 1, 1, wh) + return matcap[end - (y - 1), x] + end + return FaceIterator(cvec, faces) + elseif color isa Colorant + return FaceIterator{:Const}(color, faces) + elseif color isa AbstractArray + if color isa AbstractVector{<: Colorant} + return FaceIterator(color, faces) + elseif color isa AbstractArray{<: Number} + low, high = extrema(colorrange) + cvec = map(color[:]) do c + if isnan(c) && nan_color !== nothing + return nan_color + elseif c < low && lowclip !== nothing + return lowclip + elseif c > high && highclip !== nothing + return highclip + else + Makie.interpolated_getindex(colormap, c, colorrange) + end + end + return FaceIterator(cvec, faces) + elseif color isa AbstractMatrix{<: Colorant} && uv !== nothing + wsize = reverse(size(color)) + wh = wsize .- 1 + cvec = map(uv) do uv + x, y = clamp.(round.(Int, Tuple(uv) .* wh) .+ 1, 1, wh) + return color[end - (y - 1), x] + end + # TODO This is wrong and doesn't actually interpolate + # Inside the triangle sampling the color image + return FaceIterator(cvec, faces) + end + end + error("Unsupported Color type: $(typeof(color))") +end + +mesh_pattern_set_corner_color(pattern, id, c::Colorant) = + Cairo.mesh_pattern_set_corner_color_rgba(pattern, id, rgbatuple(c)...) diff --git a/CairoMakie/test/.gitignore b/CairoMakie/test/.gitignore new file mode 100644 index 00000000000..529f712acab --- /dev/null +++ b/CairoMakie/test/.gitignore @@ -0,0 +1,3 @@ +test_recordings +tested_different +test_formats diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl new file mode 100644 index 00000000000..3b3e4543fa2 --- /dev/null +++ b/CairoMakie/test/runtests.jl @@ -0,0 +1,72 @@ +using Test, Pkg +using CairoMakie + +# Before changing Pkg environment, try the test in #864 +@testset "Runs without error" begin + fig = Figure() + scatter(fig[1, 1], rand(10)) + fn = tempname()*".png" + try + save(fn, fig) + finally + rm(fn) + end +end + +using ReferenceTests +using ReferenceTests: database_filtered + +CairoMakie.activate!(type = "png") + +excludes = Set([ + "Colored Mesh", + "Line GIF", + "Streamplot animation", + "Line changing colour", + "Axis + Surface", + "Streamplot 3D", + "Meshscatter Function", + "Hollow pie chart", + "Record Video", + "Image on Geometry (Earth)", + "Comparing contours, image, surfaces and heatmaps", + "Textured Mesh", + "Simple pie chart", + "Animated surface and wireframe", + "Open pie chart", + "image scatter", + "surface + contour3d", + "Orthographic Camera", + "Legend", + "rotation", + "3D Contour with 2D contour slices", + "Surface with image", + "Test heatmap + image overlap", + "Text Annotation", + "step-2", + "FEM polygon 2D.png", + "Text rotation", + "Image on Surface Sphere", + "FEM mesh 2D", + "Hbox", + "Stars", + "Subscenes", + "Arrows 3D", + "Layouting", + # sigh this is actually super close, + # but doesn't interpolate the values inside the + # triangles, so looks pretty different + "FEM polygon 2D", + "Connected Sphere", + # markers too big, close otherwise, needs to be assimilated with glmakie + "Unicode Marker", +]) +excludes2 = Set(["short_tests_90", "short_tests_111", "short_tests_35", "short_tests_13", "short_tests_3"]) + +functions = [:volume, :volume!, :uv_mesh] +database = database_filtered(excludes, excludes2, functions=functions) + +recorded = joinpath(@__DIR__, "recorded") +rm(recorded; force=true, recursive=true); mkdir(recorded) +ReferenceTests.record_tests(database; recording_dir=recorded) +ReferenceTests.reference_tests(recorded) diff --git a/CairoMakie/test/saving.jl b/CairoMakie/test/saving.jl new file mode 100644 index 00000000000..19dee318903 --- /dev/null +++ b/CairoMakie/test/saving.jl @@ -0,0 +1,25 @@ +database = MakieGallery.load_database(["short_tests.jl"]); + +filter!(database) do example + !("3d" ∈ example.tags) +end + +format_save_path = joinpath(@__DIR__, "test_formats") +isdir(format_save_path) && rm(format_save_path, recursive = true) +mkpath(format_save_path) +savepath(uid, fmt) = joinpath(format_save_path, "$uid.$fmt") + +@testset "Saving formats" begin + for example in database + scene = MakieGallery.eval_example(example) + for fmt in ("png", "pdf", "svg") + @test try + save(savepath(example.unique_name, fmt), scene) + true + catch e + @warn "Saving $(example.unique_name) in format `$fmt` failed!" exception=(e, Base.catch_backtrace()) + false + end + end + end +end diff --git a/GLMakie/LICENSE.md b/GLMakie/LICENSE.md new file mode 100644 index 00000000000..ff81d888300 --- /dev/null +++ b/GLMakie/LICENSE.md @@ -0,0 +1,25 @@ +The Makie.jl package is licensed under the MIT "Expat" License: + +> Copyright (c) 2017: SimonDanisch. +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all +> copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +> SOFTWARE. +> + + +Icon made by neungstockr from www.flaticon.com diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml new file mode 100644 index 00000000000..e2a3537e1e5 --- /dev/null +++ b/GLMakie/Project.toml @@ -0,0 +1,38 @@ +name = "GLMakie" +uuid = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" +version = "0.4" + +[deps] +ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +FreeTypeAbstraction = "663a7486-cb36-511b-a19d-713bb74d65c9" +GLFW = "f7f18e0c-5ee9-5ccd-a5bf-e8befd85ed98" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118" +ModernGL = "66fc600b-dfda-50eb-8b99-91cfa97b1301" +Observables = "510215fc-4207-5dde-b226-833fc4488ee2" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +ShaderAbstractions = "65257c39-d410-5151-9873-9b3e5be5013e" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[compat] +ColorTypes = "0.9, 0.10, 0.11" +Colors = "0.11, 0.12" +FileIO = "1.1" +FixedPointNumbers = "0.7, 0.8" +FreeTypeAbstraction = "0.8, 0.9" +GLFW = "3" +GeometryBasics = "0.3" +Makie = "=0.14.0" +MeshIO = "0.4" +ModernGL = "1" +Observables = "0.4" +ShaderAbstractions = "0.2.2" +StaticArrays = "0.12, 1.0" +julia = "1" diff --git a/GLMakie/README.md b/GLMakie/README.md new file mode 100644 index 00000000000..df3eac26f28 --- /dev/null +++ b/GLMakie/README.md @@ -0,0 +1,62 @@ +The OpenGL backend for [Makie](https://github.com/JuliaPlots/Makie.jl) + +Read the docs for Makie and it's backends [here](http://makie.juliaplots.org/.dev) + +## Issues +Please file all issues in [Makie.jl](https://github.com/JuliaPlots/Makie.jl/issues/new), and mention GLMakie in the issue text! + + +## Troubleshooting OpenGL + +If you get any error loading GLMakie, it likely means, you don't have an OpenGL capable Graphic Card, or you don't have an OpenGL 3.3 capable video driver installed. +Note, that most GPUs, even 8 year old integrated ones, support OpenGL 3.3. + +On Linux, you can find out your OpenGL version with: +`glxinfo | grep "OpenGL version"` + +If you're using an AMD or Intel gpu on linux, you may run into [GLFW#198](https://github.com/JuliaGL/GLFW.jl/issues/198). + +If you're on a headless server, you still need to install x-server and +proper graphics drivers. + +You can find instructions to set that up in: + +https://nextjournal.com/sdanisch/GLMakie-nogpu +And for a headless github action: + +https://github.com/JuliaPlots/Makie.jl/blob/master/.github/workflows/glmakie.yaml +If none of these work for you, there is also a Cairo and WebGL backend +for Makie which you can use: + +https://github.com/JuliaPlots/Makie.jl/tree/master/CairoMakie. + +https://github.com/JuliaPlots/Makie.jl/tree/master/WGLMakie. + +If you get an error pointing to [GLFW.jl](https://github.com/JuliaGL/GLFW.jl), please look into the existing [GLFW issues](https://github.com/JuliaGL/GLFW.jl/issues), and also google for those errors. This is then very likely something that needs fixing in the [glfw c library](https://github.com/glfw/glfw) or in the GPU drivers. + + +## WGL setup or X-forwarding + +From: https://github.com/Microsoft/WSL/issues/2855#issuecomment-358861903 + +WSL runs OpenGL alright, but it is not a supported scenario. +From a clean Ubuntu install from the store do: + +``` +sudo apt install ubuntu-desktop mesa-utils +export DISPLAY=localhost:0 +glxgears +``` + +On the Windows side: + +1) install [VcXsrv](https://sourceforge.net/projects/vcxsrv/) +2) choose multiple windows -> display 0 -> start no client -> disable native opengl + +Troubleshooting: + +1.) install: `sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev` + +2.) WSL has some problems with passing through localhost, so one may need to use: `export DISPLAY=192.168.178.31:0`, with the local ip of the pcs network adapter, which runs VcXsrv + +3.) One may need `mv /opt/julia-1.5.2/lib/julia/libstdc++.so.6 /opt/julia-1.5.2/lib/julia/libcpp.backup`, another form of [GLFW#198](https://github.com/JuliaGL/GLFW.jl/issues/198) diff --git a/GLMakie/assets/loading.bin b/GLMakie/assets/loading.bin new file mode 100644 index 00000000000..6198c533be5 Binary files /dev/null and b/GLMakie/assets/loading.bin differ diff --git a/GLMakie/assets/shader/distance_shape.frag b/GLMakie/assets/shader/distance_shape.frag new file mode 100644 index 00000000000..afe46759ef2 --- /dev/null +++ b/GLMakie/assets/shader/distance_shape.frag @@ -0,0 +1,163 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} +{{SUPPORTED_EXTENSIONS}} + +// Half width of antialiasing smoothstep +#define ANTIALIAS_RADIUS 0.8 + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +}; + +#define CIRCLE 0 +#define RECTANGLE 1 +#define ROUNDED_RECTANGLE 2 +#define DISTANCEFIELD 3 +#define TRIANGLE 4 + +#define M_SQRT_2 1.4142135 + + +{{distancefield_type}} distancefield; +{{image_type}} image; + +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 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; +flat in int f_primitive_index; +in vec2 f_uv; // f_uv.{x,y} are in the interval [-a, 1+a] +flat in vec4 f_uv_texture_bbox; + +// These versions of aastep assume that `dist` is a signed distance function +// which has been scaled to be in units of pixels. +float aastep(float threshold1, float dist) { + return min(1.0, f_viewport_from_u_scale)*smoothstep(threshold1-ANTIALIAS_RADIUS, threshold1+ANTIALIAS_RADIUS, dist); +} +float aastep(float threshold1, float threshold2, float dist) { + return smoothstep(threshold1-ANTIALIAS_RADIUS, threshold1+ANTIALIAS_RADIUS, dist) - + smoothstep(threshold2-ANTIALIAS_RADIUS, threshold2+ANTIALIAS_RADIUS, dist); +} + +float step2(float edge1, float edge2, float value){ + return min(step(edge1, value), 1-step(edge2, value)); +} + +// Procedural signed distance functions on the uv coordinate patch [0,1]x[0,1] +// Note that for antialiasing to work properly these should be *scale preserving* +// (If you must rescale uv, make sure to put the scale factor back in later.) +float triangle(vec2 P){ + P -= vec2(0.5); + float x = M_SQRT_2 * (P.x - P.y); + float y = M_SQRT_2 * (P.x + P.y); + float r1 = max(abs(x), abs(y)) - 1./(2*M_SQRT_2); + float r2 = P.y; + return -max(r1,r2); +} +float circle(vec2 uv){ + return 0.5-length(uv-vec2(0.5)); +} +float rectangle(vec2 uv){ + vec2 d = max(-uv, uv-vec2(1)); + return -((length(max(vec2(0.0), d)) + min(0.0, max(d.x, d.y)))); +} +float rounded_rectangle(vec2 uv, vec2 tl, vec2 br){ + vec2 d = max(tl-uv, uv-br); + return -((length(max(vec2(0.0), d)) + min(0.0, max(d.x, d.y)))-tl.x); +} + +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); +} + + +void stroke(vec4 strokecolor, float signed_distance, float width, inout vec4 color){ + if (width != 0.0){ + float t = aastep(min(width, 0.0), max(width, 0.0), signed_distance); + color = mix(color, strokecolor, t); + } +} + +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; + color = mix(vec4(glowcolor.rgb, glowcolor.a*alpha), color, inside); + } +} + +float get_distancefield(sampler2D distancefield, vec2 uv){ + // Glyph distance field units are in pixels. Convert to same distance + // scaling as f_uv.x for consistency with the procedural signed_distance + // calculations. + return f_distancefield_scale * texture(distancefield, uv).r; +} +float get_distancefield(Nothing distancefield, vec2 uv){ + return 0.0; +} + +void write2framebuffer(vec4 color, uvec2 id); + +void main(){ + float signed_distance = 0.0; + + // UV coords in the texture are clamped so that they don't stray outside + // the valid subregion of the texture atlas containing the current glyph. + vec2 tex_uv = mix(f_uv_texture_bbox.xy, f_uv_texture_bbox.zw, + clamp(f_uv, 0.0, 1.0)); + + if(shape == CIRCLE) + signed_distance = circle(f_uv); + else if(shape == DISTANCEFIELD){ + signed_distance = get_distancefield(distancefield, tex_uv); + if (stroke_width > 0 || glow_width > 0) { + // Compensate for the clamping of tex_uv by an approximate + // extension of the signed distance outside the valid texture + // region. + vec2 bufuv = f_uv - clamp(f_uv, 0.0, 1.0); + signed_distance -= length(bufuv); + } + } + else if(shape == ROUNDED_RECTANGLE) + signed_distance = rounded_rectangle(f_uv, vec2(0.2), vec2(0.8)); + else if(shape == RECTANGLE) + signed_distance = 1.0; // rectangle(f_uv); + else if(shape == TRIANGLE) + signed_distance = triangle(f_uv); + + // 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 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); + // 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) + // discard; + write2framebuffer(final_color, f_id); + // Debug tools: + // * Show the background of the sprite. + // write2framebuffer(mix(final_color, vec4(1,0,0,1), 0.2), f_id); + // * Show the antialiasing border around glyphs + // write2framebuffer(vec4(vec3(abs(signed_distance)),1), f_id); +} diff --git a/GLMakie/assets/shader/dots.frag b/GLMakie/assets/shader/dots.frag new file mode 100644 index 00000000000..4f08d9a57fe --- /dev/null +++ b/GLMakie/assets/shader/dots.frag @@ -0,0 +1,10 @@ +{{GLSL_VERSION}} + +flat in vec4 o_color; +flat in uvec2 o_objectid; + +void write2framebuffer(vec4 color, uvec2 id); + +void main(){ + write2framebuffer(o_color, o_objectid); +} diff --git a/GLMakie/assets/shader/dots.vert b/GLMakie/assets/shader/dots.vert new file mode 100644 index 00000000000..beb3fa24448 --- /dev/null +++ b/GLMakie/assets/shader/dots.vert @@ -0,0 +1,40 @@ +{{GLSL_VERSION}} + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +}; + +{{vertex_type}} vertex; +{{color_type}} color; +{{color_norm_type}} color_norm; +{{color_map_type}} color_map; +uniform uint objectid; + +flat out vec4 o_color; +flat out uvec2 o_objectid; + + +float _normalize(float val, float from, float to){return (val-from) / (to - from);} + +vec4 color_lookup(float intensity, sampler1D color_ramp, vec2 norm){ + return texture(color_ramp, _normalize(intensity, norm.x, norm.y)); +} +void colorize(Nothing intensity, vec3 color, Nothing color_norm){ + o_color = vec4(color, 1); +} +void colorize(Nothing intensity, vec4 color, Nothing color_norm){ + o_color = color; +} +void colorize(sampler1D color, float intensity, vec2 color_norm){ + o_color = color_lookup(intensity, color, color_norm); +} +vec4 _position(vec3 p){return vec4(p,1);} +vec4 _position(vec2 p){return vec4(p,0,1);} + +uniform mat4 projectionview, model; + +void main(){ + colorize(color_map, color, color_norm); + o_objectid = uvec2(objectid, gl_VertexID+1); + gl_Position = projectionview * model * _position(vertex); +} diff --git a/GLMakie/assets/shader/fragment_output.frag b/GLMakie/assets/shader/fragment_output.frag new file mode 100644 index 00000000000..f334cbcb328 --- /dev/null +++ b/GLMakie/assets/shader/fragment_output.frag @@ -0,0 +1,19 @@ +{{GLSL_VERSION}} + +layout(location=0) out vec4 fragment_color; +layout(location=1) out uvec2 fragment_groupid; +{{buffers}} + + +in vec4 o_view_pos; +in vec3 o_normal; + +void write2framebuffer(vec4 color, uvec2 id){ + if(color.a <= 0.0) + discard; + // For FXAA & SSAO + fragment_color = color; + // For plot/sprite picking + fragment_groupid = id; + {{buffer_writes}} +} diff --git a/GLMakie/assets/shader/heatmap.vert b/GLMakie/assets/shader/heatmap.vert new file mode 100644 index 00000000000..0054cb5da4d --- /dev/null +++ b/GLMakie/assets/shader/heatmap.vert @@ -0,0 +1,41 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +uniform sampler1D position_x; +uniform sampler1D position_y; +in vec2 vertices; + +uniform mat4 projection, view, model; +uniform uint objectid; + +out vec2 o_uv; +flat out uvec2 o_objectid; + +out vec4 o_view_pos; +out vec3 o_normal; + +ivec2 ind2sub(ivec2 dim, int linearindex){ + return ivec2(linearindex % dim.x, linearindex / dim.x); +} + +void main(){ + //Outputs for ssao, which we don't use for 2d shaders like heatmap/image + o_view_pos = vec4(0); + o_normal = vec3(0); + + int index = gl_InstanceID; + vec2 offset = vertices; + ivec2 offseti = ivec2(offset); + ivec2 dims = ivec2(textureSize(position_x, 0), textureSize(position_y, 0)); + 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); + + o_uv = vec2(index01.x, 1.0 - index01.y); + o_objectid = uvec2(objectid, index1D+1); + + float x = texelFetch(position_x, index2D.x, 0).x; + float y = texelFetch(position_y, index2D.y, 0).x; + + gl_Position = projection * view * model * vec4(x, y, 0, 1); +} diff --git a/GLMakie/assets/shader/image.vert b/GLMakie/assets/shader/image.vert new file mode 100644 index 00000000000..45f224db587 --- /dev/null +++ b/GLMakie/assets/shader/image.vert @@ -0,0 +1,26 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +{{vertices_type}} vertices; +in vec2 texturecoordinates; + +uniform mat4 projection, view, model; +uniform uint objectid; + +out vec2 o_uv; +flat out uvec2 o_objectid; + +out vec4 o_view_pos; +out vec3 o_normal; + +vec4 _position(vec3 p){return vec4(p,1);} +vec4 _position(vec2 p){return vec4(p,0,1);} + +void main(){ + //Outputs for ssao, which we don't use for 2d shaders like heatmap/image + o_view_pos = vec4(0); + o_normal = vec3(0); + o_uv = texturecoordinates; + o_objectid = uvec2(objectid, gl_VertexID+1); + gl_Position = projection * view * model * _position(vertices); +} diff --git a/GLMakie/assets/shader/intensity.frag b/GLMakie/assets/shader/intensity.frag new file mode 100644 index 00000000000..456b575da20 --- /dev/null +++ b/GLMakie/assets/shader/intensity.frag @@ -0,0 +1,57 @@ +{{GLSL_VERSION}} + +in vec2 o_uv; +flat in uvec2 o_objectid; + +{{intensity_type}} intensity; +uniform sampler1D color_map; +uniform vec2 color_norm; + +uniform float stroke_width; +uniform vec4 stroke_color; +uniform float levels; + +uniform vec4 highclip; +uniform vec4 lowclip; +uniform vec4 nan_color; + +vec4 getindex(sampler2D image, vec2 uv){return texture(image, vec2(uv.x, 1-uv.y));} +vec4 getindex(sampler1D image, vec2 uv){return texture(image, uv.y);} +float range_01(float val, float from, float to){ + return (val - from) / (to - from); +} + +#define ALIASING_CONST 0.70710678118654757 +#define M_PI 3.1415926535897932384626433832795 + +float aastep(float threshold1, float threshold2, float value) { + float afwidth = length(vec2(dFdx(value), dFdy(value))) * ALIASING_CONST; + return smoothstep(threshold1-afwidth, threshold1+afwidth, value)-smoothstep(threshold2-afwidth, threshold2+afwidth, value); +} +float aastep(float threshold1, float value) { + float afwidth = length(vec2(dFdx(value), dFdy(value))) * ALIASING_CONST; + return smoothstep(threshold1-afwidth, threshold1+afwidth, value); +} +void write2framebuffer(vec4 color, uvec2 id); + +void main(){ + float i = float(getindex(intensity, o_uv).x); + i = range_01(i, color_norm.x, color_norm.y); + vec4 color = texture(color_map, clamp(i, 0.0, 1.0)); + if (isnan(i)) { + color = nan_color; + } else if (i < 0.0) { + color = lowclip; + } else if (i > 1.0) { + color = highclip; + } else { + if(stroke_width > 0.0){ + float lines = i * levels; + lines = abs(fract(lines - 0.5)); + float half_stroke = stroke_width * 0.5; + lines = aastep(0.5 - half_stroke, 0.5 + half_stroke, lines); + color = mix(color, stroke_color, lines); + } + } + write2framebuffer(color, uvec2(o_objectid.x, 0)); +} diff --git a/GLMakie/assets/shader/line_segment.geom b/GLMakie/assets/shader/line_segment.geom new file mode 100644 index 00000000000..1a7b232081d --- /dev/null +++ b/GLMakie/assets/shader/line_segment.geom @@ -0,0 +1,68 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +layout(lines) in; +layout(triangle_strip, max_vertices = 4) out; + +uniform vec2 resolution; +uniform float maxlength; +uniform float thickness; +uniform float pattern_length; + +in vec4 g_color[]; +in uvec2 g_id[]; +in float g_thickness[]; + +out float f_thickness; +out vec4 f_color; +out vec2 f_uv; +flat out uvec2 f_id; + +#define AA_THICKNESS 2.0 + +vec2 screen_space(vec4 vertex) +{ + return vec2(vertex.xy / vertex.w)*resolution; +} + +void emit_vertex(vec2 position, vec2 uv, int index) +{ + vec4 inpos = gl_in[index].gl_Position; + f_uv = uv; + f_color = g_color[index]; + gl_Position = vec4((position / resolution) * inpos.w, inpos.z, inpos.w); + f_id = g_id[index]; + f_thickness = g_thickness[index] + AA_THICKNESS; + EmitVertex(); +} + +uniform int max_primtives; + +out vec4 o_view_pos; +out vec3 o_normal; + +void main(void) +{ + o_view_pos = vec4(0); + o_normal = vec3(0); + // get the four vertices passed to the shader: + vec2 p0 = screen_space(gl_in[0].gl_Position); // start of previous segment + vec2 p1 = screen_space(gl_in[1].gl_Position); // end of previous segment, start of current segment + + float thickness_aa0 = g_thickness[0]+AA_THICKNESS; + float thickness_aa1 = g_thickness[1]+AA_THICKNESS; + // determine the direction of each of the 3 segments (previous, current, next) + vec2 vun0 = p1 - p0; + vec2 v0 = normalize(vun0); + // determine the normal of each of the 3 segments (previous, current, next) + vec2 n0 = vec2(-v0.y, v0.x); + float l = length(p1-p0); + l /= (pattern_length*10); + + float uv0 = thickness_aa0/g_thickness[0]; + float uv1 = thickness_aa1/g_thickness[1]; + emit_vertex(p0 + thickness_aa0 * n0, vec2(0, -uv0), 0); + emit_vertex(p0 - thickness_aa0 * n0, vec2(0, uv0), 0); + emit_vertex(p1 + thickness_aa1 * n0, vec2(l, -uv1), 1); + emit_vertex(p1 - thickness_aa1 * n0, vec2(l, uv1), 1); +} diff --git a/GLMakie/assets/shader/line_segment.vert b/GLMakie/assets/shader/line_segment.vert new file mode 100644 index 00000000000..a4ec42b868b --- /dev/null +++ b/GLMakie/assets/shader/line_segment.vert @@ -0,0 +1,44 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +}; + +{{vertex_type}} vertex; +{{thickness_type}} thickness; + +{{color_type}} color; +{{color_map_type}} color_map; +{{color_norm_type}} color_norm; + +uniform mat4 projectionview, model; +uniform uint objectid; + +out uvec2 g_id; +out vec4 g_color; +out float g_thickness; + +vec4 getindex(sampler2D tex, int index); +vec4 getindex(sampler1D tex, int index); +vec4 color_lookup(float intensity, sampler1D color_ramp, vec2 norm); + +vec4 to_vec4(vec3 v){return vec4(v, 1);} +vec4 to_vec4(vec2 v){return vec4(v, 0, 1);} + +vec4 to_color(vec4 v, Nothing color_map, Nothing color_norm, int index){return v;} +vec4 to_color(vec3 v, Nothing color_map, Nothing color_norm, int index){return vec4(v, 1);} +vec4 to_color(sampler1D tex, Nothing color_map, Nothing color_norm, int index){return getindex(tex, index);} +vec4 to_color(sampler2D tex, Nothing color_map, Nothing color_norm, int index){return getindex(tex, index);} +vec4 to_color(float color, sampler1D color_map, vec2 color_norm, int index){ + return color_lookup(color, color_map, color_norm); +} + +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; + gl_Position = projectionview * model * to_vec4(vertex); +} diff --git a/GLMakie/assets/shader/lines.frag b/GLMakie/assets/shader/lines.frag new file mode 100644 index 00000000000..fd8e569025a --- /dev/null +++ b/GLMakie/assets/shader/lines.frag @@ -0,0 +1,49 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} +{{SUPPORTED_EXTENSIONS}} + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +}; + +in vec4 f_color; +in vec2 f_uv; +in float f_thickness; +flat in uvec2 f_id; +{{pattern_type}} pattern; + +uniform float pattern_length; + +const float ALIASING_CONST = 0.9; + +float aastep(float threshold1, float value) { + float afwidth = length(vec2(dFdx(value), dFdy(value))) * ALIASING_CONST; + return smoothstep(threshold1-afwidth, threshold1+afwidth, value); +} +float aastep(float threshold1, float threshold2, float value) { + float afwidth = length(vec2(dFdx(value), dFdy(value))) * ALIASING_CONST; + return smoothstep(threshold1-afwidth, threshold1+afwidth, value)-smoothstep(threshold2-afwidth, threshold2+afwidth, value); +} +void write2framebuffer(vec4 color, uvec2 id); + +// x/y pattern +float get_sd(sampler2D pattern, vec2 uv){ + return texture(pattern, uv).x; +} +uniform float maxlength; +// x pattern +vec2 get_sd(sampler1D pattern, vec2 uv){ + return vec2(texture(pattern, uv.x).x, uv.y); +} +// normal line type +vec2 get_sd(Nothing _, vec2 uv){ + return vec2(0.5, uv.y); +} + +void main(){ + vec2 xy = get_sd(pattern, f_uv); + float alpha = aastep(0, xy.x); + float alpha2 = aastep(-1, 1, xy.y); + vec4 color = vec4(f_color.rgb, f_color.a*alpha*alpha2); + write2framebuffer(color, f_id); +} diff --git a/GLMakie/assets/shader/lines.geom b/GLMakie/assets/shader/lines.geom new file mode 100644 index 00000000000..80387847b3f --- /dev/null +++ b/GLMakie/assets/shader/lines.geom @@ -0,0 +1,170 @@ +// ------------------ Geometry Shader -------------------------------- +// This version of the line shader simply cuts off the corners and +// draws the line with no overdraw on neighboring segments at all +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +layout(lines_adjacency) in; +layout(triangle_strip, max_vertices = 7) out; + +in vec4 g_color[]; +in float g_lastlen[]; +in uvec2 g_id[]; +in uint g_line_connections[]; +in int g_valid_vertex[]; +//in float g_thickness[]; + +out vec4 f_color; +out vec2 f_uv; +out float f_thickness; + +flat out uvec2 f_id; + +uniform vec2 resolution; +uniform float maxlength; +uniform float thickness; +uniform float pattern_length; + + +#define MITER_LIMIT -0.4 + +vec2 screen_space(vec4 vertex) +{ + return vec2(vertex.xy / vertex.w) * resolution; +} +void emit_vertex(vec2 position, vec2 uv, int index, float ratio) +{ + vec4 inpos = gl_in[index].gl_Position; + f_uv = vec2((g_lastlen[index] * ratio) / pattern_length / (thickness+4) / 2.0, uv.y); + f_color = g_color[index]; + gl_Position = vec4((position/resolution)*inpos.w, inpos.z, inpos.w); + f_id = g_id[index]; + f_thickness = thickness; + EmitVertex(); +} + +uniform int max_primtives; +const float infinity = 1.0 / 0.0; + +out vec4 o_view_pos; +out vec3 o_normal; + +void main(void) +{ + o_view_pos = vec4(0); + o_normal = vec3(0); + // We mark each of the four vertices as valid or not. Vertices can be + // marked invalid on input (eg, if they contain NaN). We also mark them + // invalid if they repeat in the index buffer. This allows us to render to + // the very ends of a polyline without clumsy buffering the position data on the + // CPU side by repeating the first and last points via the index buffer. It + // just requires a little care further down to avoid degenerate normals. + bool isvalid[4] = bool[]( + g_valid_vertex[0] == 1 && g_id[0].y != g_id[1].y, + g_valid_vertex[1] == 1, + g_valid_vertex[2] == 1, + g_valid_vertex[3] == 1 && g_id[2].y != g_id[3].y + ); + + if(g_line_connections[1] != g_line_connections[2] || !isvalid[1] || !isvalid[2]){ + // If one of the central vertices is invalid or there is a break in the + // line, we don't emit anything. + return; + } + // get the four vertices passed to the shader: + vec2 p0 = screen_space(gl_in[0].gl_Position); // start of previous segment + vec2 p1 = screen_space(gl_in[1].gl_Position); // end of previous segment, start of current segment + vec2 p2 = screen_space(gl_in[2].gl_Position); // end of current segment, start of next segment + vec2 p3 = screen_space(gl_in[3].gl_Position); // end of next segment + + float thickness_aa = thickness+4; + + + // perform naive culling + //vec2 area = resolution * 1.2; + //if( p1.x < -area.x || p1.x > area.x ) return; + //if( p1.y < -area.y || p1.y > area.y ) return; + //if( p2.x < -area.x || p2.x > area.x ) return; + //if( p2.y < -area.y || p2.y > area.y ) return; + + // determine the direction of each of the 3 segments (previous, current, next) + vec2 v1 = normalize(p2 - p1); + vec2 v0 = isvalid[0] ? normalize(p1 - p0) : v1; + vec2 v2 = isvalid[3] ? normalize(p3 - p2) : v1; + + // determine the normal of each of the 3 segments (previous, current, next) + vec2 n0 = vec2(-v0.y, v0.x); + vec2 n1 = vec2(-v1.y, v1.x); + vec2 n2 = vec2(-v2.y, v2.x); + + + // The goal here is to make wide line segments join cleanly. For most + // joints, it's enough to extend/contract the buffered lines into the + // "normal miter" shape below. However, this can get really spiky if the + // lines are almost anti-parallel, in which case we want the truncated + // mitre. For the truncated miter, we must emit the additional triangle + // x-a-b. + // + // normal miter truncated miter + // ------------------* ----------a. + // / | '. + // x / x_ '. + // ------* / ------. '--b + // / / / / + // / / / / + // + // Note that the way this is done below is fairly simple but results in + // overdraw for semi transparent lines. Ideally would be nice to fix that + // somehow. + + // determine miter lines by averaging the normals of the 2 segments + vec2 miter_a = normalize(n0 + n1); // miter at start of current segment + vec2 miter_b = normalize(n1 + n2); // miter at end of current segment + + // determine the length of the miter by projecting it onto normal and then inverse it + float length_a = thickness_aa / dot(miter_a, n1); + float length_b = thickness_aa / dot(miter_b, n1); + + float xstart = g_lastlen[1]; + float xend = g_lastlen[2]; + float ratio = length(p2 - p1) / (xend - xstart); + + float uvy = thickness_aa/thickness; + + if( dot( v0, v1 ) < MITER_LIMIT ){ + /* + n1 + gap true : gap false + v0 : + . ------> : + */ + bool gap = dot( v0, n1 ) > 0; + // close the gap + if(gap){ + emit_vertex(p1 + thickness_aa * n0, vec2(1, -uvy), 1, ratio); + emit_vertex(p1 + thickness_aa * n1, vec2(1, -uvy), 1, ratio); + emit_vertex(p1, vec2(0, 0.0), 1, ratio); + EndPrimitive(); + }else{ + emit_vertex(p1 - thickness_aa * n0, vec2(1, uvy), 1, ratio); + emit_vertex(p1, vec2(0, 0.0), 1, ratio); + emit_vertex(p1 - thickness_aa * n1, vec2(1, uvy), 1, ratio); + EndPrimitive(); + } + miter_a = n1; + length_a = thickness_aa; + } + + if( dot( v1, v2 ) < MITER_LIMIT ) { + miter_b = n1; + length_b = thickness_aa; + } + + // generate the triangle strip + + emit_vertex(p1 + length_a * miter_a, vec2( 0, -uvy), 1, ratio); + emit_vertex(p1 - length_a * miter_a, vec2( 0, uvy), 1, ratio); + + emit_vertex(p2 + length_b * miter_b, vec2( 0, -uvy ), 2, ratio); + emit_vertex(p2 - length_b * miter_b, vec2( 0, uvy), 2, ratio); +} diff --git a/GLMakie/assets/shader/lines.vert b/GLMakie/assets/shader/lines.vert new file mode 100644 index 00000000000..54a12ec71c8 --- /dev/null +++ b/GLMakie/assets/shader/lines.vert @@ -0,0 +1,53 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +}; + +{{vertex_type}} vertex; + + +in float lastlen; +{{valid_vertex_type}} valid_vertex; + +{{color_type}} color; +{{color_map_type}} color_map; +{{intensity_type}} intensity; +{{color_norm_type}} color_norm; + +vec4 _color(vec3 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len); +vec4 _color(vec4 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len); +vec4 _color(Nothing color, float intensity, sampler1D color_map, vec2 color_norm, int index, int len); +vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_norm, int index, int len); + +uniform mat4 projection, view, model; +uniform uint objectid; +uniform ivec2 dims; + +out uvec2 g_id; +out vec4 g_color; +out float g_lastlen; +out int g_valid_vertex; +out uint g_line_connections; + +vec4 getindex(sampler2D tex, int index); +vec4 getindex(sampler1D tex, int index); + +vec4 to_vec4(vec3 v){return vec4(v, 1);} +vec4 to_vec4(vec2 v){return vec4(v, 0, 1);} + +int get_valid_vertex(float se){return int(se);} +int get_valid_vertex(Nothing se){return 1;} + + +void main() +{ + g_lastlen = lastlen; + int index = gl_VertexID; + g_id = uvec2(objectid, index+1); + g_valid_vertex = get_valid_vertex(valid_vertex); + g_color = _color(color, intensity, color_map, color_norm, index, dims.x*dims.y); + g_line_connections = uint(index/dims.x); + gl_Position = projection*view*model*to_vec4(vertex); +} diff --git a/GLMakie/assets/shader/parametric.frag b/GLMakie/assets/shader/parametric.frag new file mode 100644 index 00000000000..46e7f5ee292 --- /dev/null +++ b/GLMakie/assets/shader/parametric.frag @@ -0,0 +1,54 @@ +{{GLSL_VERSION}} + +float rand(vec2 co){ + // implementation found at: lumina.sourceforge.net/Tutorials/Noise.html + return fract(sin(dot(co.xy, vec2(12.9898,78.233))) * 43758.5453); +} + +// Put your user defined function here... +{{function}} + +uniform float jitter = 1.0; +uniform float thickness = 2000; +uniform int samples = 8; + +in vec2 aa_scale; + + +float getalpha(vec2 pos) { + vec2 step = thickness*vec2(aa_scale.x,aa_scale.y)/samples; + float samples = float(samples); + int count = 0; + int mysamples = 0; + for (float i = 0.0; i < samples; i++) { + for (float j = 0.0;j < samples; j++) { + if (i*i+j*j>samples*samples) continue; + mysamples++; + float ii = i + jitter*rand(vec2(pos.x + i*step.x,pos.y + j*step.y)); + float jj = j + jitter*rand(vec2(pos.y + i*step.x,pos.x + j*step.y)); + float f = function(pos.x+ ii*step.x)-(pos.y+ jj*step.y); + count += (f>0.) ? 1 : -1; + } + } + if (abs(count)!=mysamples) return 1-abs(float(count))/float(mysamples); + return 0.0; +} + +in vec2 o_uv; +uniform vec4 color; + +void write2framebuffer(vec4 color, uvec2 id); + +void main() +{ + write2framebuffer( + vec4(color.rgb, color.a*getalpha(vec2(o_uv.x*5, o_uv.y))), + uvec2(0) + ); +} + + +/* +//note: shadertoy-pluggable, http://www.iquilezles.org/apps/shadertoy/ + +*/ diff --git a/GLMakie/assets/shader/parametric.vert b/GLMakie/assets/shader/parametric.vert new file mode 100644 index 00000000000..26b00afd873 --- /dev/null +++ b/GLMakie/assets/shader/parametric.vert @@ -0,0 +1,18 @@ +{{GLSL_VERSION}} + +in vec2 vertices; +in vec2 texturecoordinates; + +out vec2 o_uv; +out vec2 aa_scale; + +uniform vec2 resolution; +uniform float AntiAliasScale = 0.75; +uniform float Zoom = 1.; + +uniform mat4 projection, projectionview, model; +void main(){ + o_uv = texturecoordinates; + aa_scale = vec2(projection[0][0],projection[1][1])*(1.0/resolution)*AntiAliasScale/Zoom; + gl_Position = projectionview * model * vec4(vertices, 0, 1); +} diff --git a/GLMakie/assets/shader/particles.vert b/GLMakie/assets/shader/particles.vert new file mode 100644 index 00000000000..ce79aede07f --- /dev/null +++ b/GLMakie/assets/shader/particles.vert @@ -0,0 +1,150 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +}; +struct Grid1D{ + int lendiv; + float start; + float stop; + int dims; +}; +struct Grid2D{ + ivec2 lendiv; + vec2 start; + vec2 stop; + ivec2 dims; + +}; +struct Grid3D{ + ivec3 lendiv; + vec3 start; + vec3 stop; + ivec3 dims; +}; + +in vec3 vertices; +in vec3 normals; +{{texturecoordinates_type}} texturecoordinates; + +uniform vec3 lightposition; +uniform mat4 view, model, projection; +uniform uint objectid; +uniform int len; + +flat out uvec2 o_id; +out vec4 o_color; +out vec2 o_uv; + +{{position_type}} position; +{{position_x_type}} position_x; +{{position_y_type}} position_y; +{{position_z_type}} position_z; + +ivec2 ind2sub(ivec2 dim, int linearindex); +ivec3 ind2sub(ivec3 dim, int linearindex); + + +{{rotation_type}} rotation; +void rotate(Nothing vectors, int index, inout vec3 vertices, inout vec3 normal); +void rotate(samplerBuffer vectors, int index, inout vec3 V, inout vec3 N); +void rotate(vec4 vectors, int index, inout vec3 vertices, inout vec3 normal); + + +{{scale_type}} scale; // so in the case of distinct x,y,z, there's no chance to unify them under one variable +{{scale_x_type}} scale_x; +{{scale_y_type}} scale_y; +{{scale_z_type}} scale_z; +vec4 get_rotation(samplerBuffer rotation, int index){ + return texelFetch(rotation, index); +} +vec4 get_rotation(Nothing rotation, int index){ + return vec4(0,0,0,1); +} +vec4 get_rotation(vec4 rotation, int index){ + return rotation; +} +vec3 _scale(samplerBuffer scale, Nothing scale_x, Nothing scale_y, Nothing scale_z, int index); +vec3 _scale(vec3 scale, float scale_x, samplerBuffer scale_y, float scale_z, int index); +vec3 _scale(Nothing scale, float scale_x, samplerBuffer scale_y, float scale_z, int index); +vec3 _scale(vec3 scale, float scale_x, float scale_y, samplerBuffer scale_z, int index); +vec3 _scale(Nothing scale, float scale_x, float scale_y, samplerBuffer scale_z, int index); + +vec3 _scale(Nothing scale, float scale_x, float scale_y, Nothing scale_z, int index){ + vec4 rot = get_rotation(rotation, index); + return vec3(scale_x, scale_y, length(rot)); +} +vec3 _scale(vec2 scale, Nothing scale_x, Nothing scale_y, Nothing scale_z, int index); + +vec3 _scale(vec3 scale, Nothing scale_x, Nothing scale_y, Nothing scale_z, int index){ + return scale; +} + +{{color_type}} color; +{{color_map_type}} color_map; +{{intensity_type}} intensity; +{{color_norm_type}} color_norm; +{{vertex_color_type}} vertex_color; +vec4 to_color(Nothing c){return vec4(1, 1, 1, 1);} +vec4 to_color(vec3 c){return vec4(c, 1);} +vec4 to_color(vec4 c){return c;} + +// constant color! +vec4 _color(vec4 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len); +vec4 _color(vec3 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len); +// only a samplerBuffer, this means we have a color per particle +vec4 _color(samplerBuffer color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len); +// no color, but intensities a color map and color norm. Color will be based on intensity! +vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_norm, int index, int len); +vec4 _color(Nothing color, samplerBuffer intensity, sampler1D color_map, vec2 color_norm, int index, int len); +// no color, no intensities a color map and color norm. Color will be based on z_position or rotation! +vec4 _color(Nothing color, Nothing intensity, sampler1D color_map, vec2 color_norm, int index, int len); + +float get_intensity(samplerBuffer rotation, Nothing position_z, int index){ + return texelFetch(rotation, index).w; +} +float get_intensity(vec4 rotation, Nothing position_z, int index){return length(rotation);} +float get_intensity(Nothing rotation, Nothing position_z, int index){return -1.0;} +float get_intensity(Nothing rotation, samplerBuffer position_z, int index){ + return texelFetch(position_z, index).x; +} +float get_intensity(vec4 rotation, samplerBuffer position_z, int index){ + return texelFetch(position_z, index).x; +} +vec4 color_lookup(float intensity, sampler1D color_ramp, vec2 norm); + +float get_intensity(vec4 rotation, float scale_z, int index){ + return scale_z; +} + +vec4 _color(Nothing color, Nothing intensity, sampler1D color_map, vec2 color_norm, int index, int len){ + float _intensity = get_intensity(rotation, scale_z, index); + return color_lookup(_intensity, color_map, color_norm); +} + + +vec4 _color(sampler2D color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len){ + return vec4(0); +} + +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition); + + +vec2 get_uv(Nothing x){return vec2(0.0);} +vec2 get_uv(vec2 x){return vec2(1.0 - x.y, x.x);} + +void main(){ + int index = gl_InstanceID; + o_id = uvec2(objectid, index+1); + vec3 s = _scale(scale, scale_x, scale_y, scale_z, index); + vec3 V = vertices * s; + vec3 N = normals; + vec3 pos; + {{position_calc}} + o_color = _color(color, intensity, color_map, color_norm, index, len); + 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); +} diff --git a/GLMakie/assets/shader/postprocessing/SSAO.frag b/GLMakie/assets/shader/postprocessing/SSAO.frag new file mode 100644 index 00000000000..97f6e326bf2 --- /dev/null +++ b/GLMakie/assets/shader/postprocessing/SSAO.frag @@ -0,0 +1,91 @@ +{{GLSL_VERSION}} + +// SSAO +uniform sampler2D position_buffer; +uniform sampler2D normal_occlusion_buffer; +uniform sampler2D noise; +uniform vec3 kernel[{{N_samples}}]; +uniform vec2 noise_scale; +uniform mat4 projection; + +// bias/epsilon for depth check +uniform float bias; +// max range for depth check +uniform float radius; + + +in vec2 frag_uv; +// occlusion.xyz is a normal vector, occlusion.w the occlusion value +out vec4 o_normal_occlusion; + + +void main(void) +{ + vec3 view_pos = texture(position_buffer, frag_uv).xyz; + vec3 normal = texture(normal_occlusion_buffer, frag_uv).xyz; + + // The normal buffer gets cleared every frame. (also position, color etc) + // If normal == vec3(1) then there is no geometry at this fragment. + // Therefore skip SSAO calculation + if (normal != vec3(1)) { + vec3 rand_vec = vec3(texture(noise, frag_uv * noise_scale).xy, 0.0); + vec3 tangent = normalize(rand_vec - normal * dot(rand_vec, normal)); + vec3 bitangent = cross(normal, tangent); + mat3 TBN = mat3(tangent, bitangent, normal); + + float occlusion = 0.0; + for (int i = 0; i < {{N_samples}}; ++i) { + // random offset in view space + vec3 sample_view_offset = TBN * kernel[i] * radius; + + /* + We want to get the uv (screen) coordinate of position + offset in + view space. Usually this would be: + clip_coordinate = projection * view_coordinate + clip_coordinate /= clip_coordinate.w + screen_coordinate = 0.5 * clip_coordinate + 0.5 + + But Makie allows multiple scenes, which each have their own + coordinate system. This means it is possible that multiple + regions of the screen (different scenes) refer to the same view + position. To differentiate between them we must calculate the + screen space coordinate using frag_uv and an offset derived from + the view space offset. + + Instead of + + clip_coord = projection * (view_pos + view_offset) + clip_coord /= clip_coord.w + screen_coordinate = 0.5 * clip_coord + 0.5 + + we essentially calculate + + clip_offset = projection * view_offset + clip_offset /= (projection * (view_pos + view_offset)).w + clip_position = frag_uv - 0.5 + clip_position *= (projection * view_pos).w + clip_position /= (projection * (view_pos + view_offset)).w + screen_coordinate = clip_position + 0.5 * clip_offset+ 0.5 + */ + + vec4 sample_frag_pos = vec4( + (projection * vec4(sample_view_offset, 1.0)).xyz, + (projection * vec4(view_pos + sample_view_offset, 1.0)).w + ); + float sample_clip_pos_w = sample_frag_pos.w; + float clip_pos_w = (projection * vec4(view_pos, 1.0)).w; + sample_frag_pos.xyz /= sample_frag_pos.w; + sample_frag_pos.xyz = sample_frag_pos.xyz * 0.5 + 0.5; + sample_frag_pos.xy += (frag_uv - 0.5) * clip_pos_w / sample_clip_pos_w; + + + float sample_depth = texture(position_buffer, sample_frag_pos.xy).z; + float range_check = smoothstep(0.0, 1.0, radius / abs(view_pos.z - sample_depth)); + occlusion += (sample_depth >= sample_view_offset.z + view_pos.z + bias ? 1.0 : 0.0) * range_check; + } + occlusion = 1.0 - (occlusion / {{N_samples}}); + o_normal_occlusion.w = occlusion; + } else { + o_normal_occlusion.w = 1.0; + } +} diff --git a/GLMakie/assets/shader/postprocessing/SSAO_blur.frag b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag new file mode 100644 index 00000000000..c26a2a5e394 --- /dev/null +++ b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag @@ -0,0 +1,39 @@ +{{GLSL_VERSION}} + +// occlusion.w is the occlusion value +uniform sampler2D normal_occlusion; +uniform sampler2D color_texture; +uniform usampler2D ids; +uniform vec2 inv_texel_size; +// Settings/Attributes +uniform int blur_range; + +in vec2 frag_uv; +out vec4 fragment_color; + +void main(void) +{ + // occlusion blur + float blurred_occlusion = 0.0; + uvec2 id0 = texture(ids, frag_uv).xy; + float weight = 0; + + for (int x = -blur_range; x <= blur_range; ++x){ + for (int y = -blur_range; y <= blur_range; ++y){ + vec2 offset = vec2(float(x), float(y)) * inv_texel_size; + // The id check makes it so that the blur acts per object. + // Without this, a high (low) occlusion from one object can bleed + // into the low (high) occlusion of another, giving an unwanted + // shine effect. + uvec2 id = texture(ids, frag_uv + offset).xy; + if (id0 == id) { + blurred_occlusion += texture(normal_occlusion, frag_uv + offset).w; + weight += 1; + } + } + } + blurred_occlusion = blurred_occlusion / weight; + fragment_color = texture(color_texture, frag_uv) * blurred_occlusion; + // Display occlusion instead: + // fragment_color = vec4(vec3(blurred_occlusion), 1.0); +} diff --git a/GLMakie/assets/shader/postprocessing/copy.frag b/GLMakie/assets/shader/postprocessing/copy.frag new file mode 100644 index 00000000000..9ec63ec90af --- /dev/null +++ b/GLMakie/assets/shader/postprocessing/copy.frag @@ -0,0 +1,12 @@ +{{GLSL_VERSION}} + +uniform sampler2D color_texture; +in vec2 frag_uv; +out vec4 fragment_color; + +void main(void) +{ + vec4 color = texture(color_texture, frag_uv); + fragment_color.rgb = color.rgb; + fragment_color.a = 1.0; +} diff --git a/GLMakie/assets/shader/postprocessing/fullscreen.vert b/GLMakie/assets/shader/postprocessing/fullscreen.vert new file mode 100644 index 00000000000..aba2c5e9b95 --- /dev/null +++ b/GLMakie/assets/shader/postprocessing/fullscreen.vert @@ -0,0 +1,15 @@ +{{GLSL_VERSION}} + +out vec2 frag_uv; + +void main() { + vec2 uv = vec2(0,0); + if((gl_VertexID & 1) != 0) + uv.x = 1; + if((gl_VertexID & 2) != 0) + uv.y = 1; + + frag_uv = uv * 2; + gl_Position.xy = (uv * 4) - 1; + gl_Position.zw = vec2(0,1); +} diff --git a/GLMakie/assets/shader/postprocessing/fxaa.frag b/GLMakie/assets/shader/postprocessing/fxaa.frag new file mode 100644 index 00000000000..56d4eb678b9 --- /dev/null +++ b/GLMakie/assets/shader/postprocessing/fxaa.frag @@ -0,0 +1,1050 @@ +{{GLSL_VERSION}} + +#define FXAA_PC 1 +#define FXAA_GLSL_130 1 +#define FXAA_QUALITY__PRESET 12 +#define FXAA_GREEN_AS_LUMA 0 +#define FXAA_GATHER4_ALPHA 0 +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_PC_CONSOLE + // + // The console algorithm for PC is included + // for developers targeting really low spec machines. + // Likely better to just run FXAA_PC, and use a really low preset. + // + #define FXAA_PC_CONSOLE 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_GLSL_120 + #define FXAA_GLSL_120 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_GLSL_130 + #define FXAA_GLSL_130 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_HLSL_3 + #define FXAA_HLSL_3 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_HLSL_4 + #define FXAA_HLSL_4 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_HLSL_5 + #define FXAA_HLSL_5 0 +#endif +/*==========================================================================*/ +#ifndef FXAA_GREEN_AS_LUMA + // + // For those using non-linear color, + // and either not able to get luma in alpha, or not wanting to, + // this enables FXAA to run using green as a proxy for luma. + // So with this enabled, no need to pack luma in alpha. + // + // This will turn off AA on anything which lacks some amount of green. + // Pure red and blue or combination of only R and B, will get no AA. + // + // Might want to lower the settings for both, + // fxaaConsoleEdgeThresholdMin + // fxaaQualityEdgeThresholdMin + // In order to insure AA does not get turned off on colors + // which contain a minor amount of green. + // + // 1 = On. + // 0 = Off. + // + #define FXAA_GREEN_AS_LUMA 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_EARLY_EXIT + // + // Controls algorithm's early exit path. + // On PS3 turning this ON adds 2 cycles to the shader. + // On 360 turning this OFF adds 10ths of a millisecond to the shader. + // Turning this off on console will result in a more blurry image. + // So this defaults to on. + // + // 1 = On. + // 0 = Off. + // + #define FXAA_EARLY_EXIT 1 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_DISCARD + // + // Only valid for PC OpenGL currently. + // Probably will not work when FXAA_GREEN_AS_LUMA = 1. + // + // 1 = Use discard on pixels which don't need AA. + // For APIs which enable concurrent TEX+ROP from same surface. + // 0 = Return unchanged color on pixels which don't need AA. + // + #define FXAA_DISCARD 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_FAST_PIXEL_OFFSET + // + // Used for GLSL 120 only. + // + // 1 = GL API supports fast pixel offsets + // 0 = do not use fast pixel offsets + // + #ifdef GL_EXT_gpu_shader4 + #define FXAA_FAST_PIXEL_OFFSET 1 + #endif + #ifdef GL_NV_gpu_shader5 + #define FXAA_FAST_PIXEL_OFFSET 1 + #endif + #ifdef GL_ARB_gpu_shader5 + #define FXAA_FAST_PIXEL_OFFSET 1 + #endif + #ifndef FXAA_FAST_PIXEL_OFFSET + #define FXAA_FAST_PIXEL_OFFSET 0 + #endif +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_GATHER4_ALPHA + // + // 1 = API supports gather4 on alpha channel. + // 0 = API does not support gather4 on alpha channel. + // + #if (FXAA_HLSL_5 == 1) + #define FXAA_GATHER4_ALPHA 1 + #endif + #ifdef GL_ARB_gpu_shader5 + #define FXAA_GATHER4_ALPHA 1 + #endif + #ifdef GL_NV_gpu_shader5 + #define FXAA_GATHER4_ALPHA 1 + #endif + #ifndef FXAA_GATHER4_ALPHA + #define FXAA_GATHER4_ALPHA 0 + #endif +#endif + + +/*============================================================================ + FXAA QUALITY - TUNING KNOBS +------------------------------------------------------------------------------ +NOTE the other tuning knobs are now in the shader function inputs! +============================================================================*/ +#ifndef FXAA_QUALITY__PRESET + // + // Choose the quality preset. + // This needs to be compiled into the shader as it effects code. + // Best option to include multiple presets is to + // in each shader define the preset, then include this file. + // + // OPTIONS + // ----------------------------------------------------------------------- + // 10 to 15 - default medium dither (10=fastest, 15=highest quality) + // 20 to 29 - less dither, more expensive (20=fastest, 29=highest quality) + // 39 - no dither, very expensive + // + // NOTES + // ----------------------------------------------------------------------- + // 12 = slightly faster then FXAA 3.9 and higher edge quality (default) + // 13 = about same speed as FXAA 3.9 and better than 12 + // 23 = closest to FXAA 3.9 visually and performance wise + // _ = the lowest digit is directly related to performance + // _ = the highest digit is directly related to style + // + #define FXAA_QUALITY__PRESET 12 +#endif + + +/*============================================================================ + + FXAA QUALITY - PRESETS + +============================================================================*/ + +/*============================================================================ + FXAA QUALITY - MEDIUM DITHER PRESETS +============================================================================*/ +#if (FXAA_QUALITY__PRESET == 10) + #define FXAA_QUALITY__PS 3 + #define FXAA_QUALITY__P0 1.5 + #define FXAA_QUALITY__P1 3.0 + #define FXAA_QUALITY__P2 12.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 11) + #define FXAA_QUALITY__PS 4 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 3.0 + #define FXAA_QUALITY__P3 12.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 12) + #define FXAA_QUALITY__PS 5 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 4.0 + #define FXAA_QUALITY__P4 12.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 13) + #define FXAA_QUALITY__PS 6 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 4.0 + #define FXAA_QUALITY__P5 12.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 14) + #define FXAA_QUALITY__PS 7 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 4.0 + #define FXAA_QUALITY__P6 12.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 15) + #define FXAA_QUALITY__PS 8 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 4.0 + #define FXAA_QUALITY__P7 12.0 +#endif + +/*============================================================================ + FXAA QUALITY - LOW DITHER PRESETS +============================================================================*/ +#if (FXAA_QUALITY__PRESET == 20) + #define FXAA_QUALITY__PS 3 + #define FXAA_QUALITY__P0 1.5 + #define FXAA_QUALITY__P1 2.0 + #define FXAA_QUALITY__P2 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 21) + #define FXAA_QUALITY__PS 4 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 22) + #define FXAA_QUALITY__PS 5 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 23) + #define FXAA_QUALITY__PS 6 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 24) + #define FXAA_QUALITY__PS 7 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 3.0 + #define FXAA_QUALITY__P6 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 25) + #define FXAA_QUALITY__PS 8 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 4.0 + #define FXAA_QUALITY__P7 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 26) + #define FXAA_QUALITY__PS 9 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 2.0 + #define FXAA_QUALITY__P7 4.0 + #define FXAA_QUALITY__P8 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 27) + #define FXAA_QUALITY__PS 10 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 2.0 + #define FXAA_QUALITY__P7 2.0 + #define FXAA_QUALITY__P8 4.0 + #define FXAA_QUALITY__P9 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 28) + #define FXAA_QUALITY__PS 11 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 2.0 + #define FXAA_QUALITY__P7 2.0 + #define FXAA_QUALITY__P8 2.0 + #define FXAA_QUALITY__P9 4.0 + #define FXAA_QUALITY__P10 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 29) + #define FXAA_QUALITY__PS 12 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 2.0 + #define FXAA_QUALITY__P7 2.0 + #define FXAA_QUALITY__P8 2.0 + #define FXAA_QUALITY__P9 2.0 + #define FXAA_QUALITY__P10 4.0 + #define FXAA_QUALITY__P11 8.0 +#endif + +/*============================================================================ + FXAA QUALITY - EXTREME QUALITY +============================================================================*/ +#if (FXAA_QUALITY__PRESET == 39) + #define FXAA_QUALITY__PS 12 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.0 + #define FXAA_QUALITY__P2 1.0 + #define FXAA_QUALITY__P3 1.0 + #define FXAA_QUALITY__P4 1.0 + #define FXAA_QUALITY__P5 1.5 + #define FXAA_QUALITY__P6 2.0 + #define FXAA_QUALITY__P7 2.0 + #define FXAA_QUALITY__P8 2.0 + #define FXAA_QUALITY__P9 2.0 + #define FXAA_QUALITY__P10 4.0 + #define FXAA_QUALITY__P11 8.0 +#endif + + + +/*============================================================================ + + API PORTING + +============================================================================*/ +#if (FXAA_GLSL_120 == 1) || (FXAA_GLSL_130 == 1) + #define FxaaBool bool + #define FxaaDiscard discard + #define FxaaFloat float + #define FxaaFloat2 vec2 + #define FxaaFloat3 vec3 + #define FxaaFloat4 vec4 + #define FxaaHalf float + #define FxaaHalf2 vec2 + #define FxaaHalf3 vec3 + #define FxaaHalf4 vec4 + #define FxaaInt2 ivec2 + #define FxaaSat(x) clamp(x, 0.0, 1.0) + #define FxaaTex sampler2D +#else + #define FxaaBool bool + #define FxaaDiscard clip(-1) + #define FxaaFloat float + #define FxaaFloat2 float2 + #define FxaaFloat3 float3 + #define FxaaFloat4 float4 + #define FxaaHalf half + #define FxaaHalf2 half2 + #define FxaaHalf3 half3 + #define FxaaHalf4 half4 + #define FxaaSat(x) saturate(x) +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_GLSL_120 == 1) + // Requires, + // #version 120 + // And at least, + // #extension GL_EXT_gpu_shader4 : enable + // (or set FXAA_FAST_PIXEL_OFFSET 1 to work like DX9) + #define FxaaTexTop(t, p) texture2DLod(t, p, 0.0) + #if (FXAA_FAST_PIXEL_OFFSET == 1) + #define FxaaTexOff(t, p, o, r) texture2DLodOffset(t, p, 0.0, o) + #else + #define FxaaTexOff(t, p, o, r) texture2DLod(t, p + (o * r), 0.0) + #endif + #if (FXAA_GATHER4_ALPHA == 1) + // use #extension GL_ARB_gpu_shader5 : enable + #define FxaaTexAlpha4(t, p) textureGather(t, p, 3) + #define FxaaTexOffAlpha4(t, p, o) textureGatherOffset(t, p, o, 3) + #define FxaaTexGreen4(t, p) textureGather(t, p, 1) + #define FxaaTexOffGreen4(t, p, o) textureGatherOffset(t, p, o, 1) + #endif +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_GLSL_130 == 1) + // Requires "#version 130" or better + #define FxaaTexTop(t, p) textureLod(t, p, 0.0) + #define FxaaTexOff(t, p, o, r) textureLodOffset(t, p, 0.0, o) + #if (FXAA_GATHER4_ALPHA == 1) + // use #extension GL_ARB_gpu_shader5 : enable + #define FxaaTexAlpha4(t, p) textureGather(t, p, 3) + #define FxaaTexOffAlpha4(t, p, o) textureGatherOffset(t, p, o, 3) + #define FxaaTexGreen4(t, p) textureGather(t, p, 1) + #define FxaaTexOffGreen4(t, p, o) textureGatherOffset(t, p, o, 1) + #endif +#endif +/*--------------------------------------------------------------------------*/ + +/*--------------------------------------------------------------------------*/ +#if (FXAA_HLSL_5 == 1) + #define FxaaInt2 int2 + struct FxaaTex { SamplerState smpl; Texture2D tex; }; + #define FxaaTexTop(t, p) t.tex.SampleLevel(t.smpl, p, 0.0) + #define FxaaTexOff(t, p, o, r) t.tex.SampleLevel(t.smpl, p, 0.0, o) + #define FxaaTexAlpha4(t, p) t.tex.GatherAlpha(t.smpl, p) + #define FxaaTexOffAlpha4(t, p, o) t.tex.GatherAlpha(t.smpl, p, o) + #define FxaaTexGreen4(t, p) t.tex.GatherGreen(t.smpl, p) + #define FxaaTexOffGreen4(t, p, o) t.tex.GatherGreen(t.smpl, p, o) +#endif + + +/*============================================================================ + GREEN AS LUMA OPTION SUPPORT FUNCTION +============================================================================*/ +#if (FXAA_GREEN_AS_LUMA == 0) + FxaaFloat FxaaLuma(FxaaFloat4 rgba) { return rgba.a; } +#else + FxaaFloat FxaaLuma(FxaaFloat4 rgba) { return rgba.y; } +#endif + + + + +/*============================================================================ + + FXAA3 QUALITY - PC + +============================================================================*/ +#if (FXAA_PC == 1) +/*--------------------------------------------------------------------------*/ +FxaaFloat4 FxaaPixelShader( + // + // Use noperspective interpolation here (turn off perspective interpolation). + // {xy} = center of pixel + FxaaFloat2 pos, + // + // Used only for FXAA Console, and not used on the 360 version. + // Use noperspective interpolation here (turn off perspective interpolation). + // {xy__} = upper left of pixel + // {__zw} = lower right of pixel + FxaaFloat4 fxaaConsolePosPos, + // + // Input color texture. + // {rgb_} = color in linear or perceptual color space + // if (FXAA_GREEN_AS_LUMA == 0) + // {___a} = luma in perceptual color space (not linear) + FxaaTex tex, + // + // Only used on the optimized 360 version of FXAA Console. + // For everything but 360, just use the same input here as for "tex". + // For 360, same texture, just alias with a 2nd sampler. + // This sampler needs to have an exponent bias of -1. + FxaaTex fxaaConsole360TexExpBiasNegOne, + // + // Only used on the optimized 360 version of FXAA Console. + // For everything but 360, just use the same input here as for "tex". + // For 360, same texture, just alias with a 3nd sampler. + // This sampler needs to have an exponent bias of -2. + FxaaTex fxaaConsole360TexExpBiasNegTwo, + // + // Only used on FXAA Quality. + // This must be from a constant/uniform. + // {x_} = 1.0/screenWidthInPixels + // {_y} = 1.0/screenHeightInPixels + FxaaFloat2 fxaaQualityRcpFrame, + // + // Only used on FXAA Console. + // This must be from a constant/uniform. + // This effects sub-pixel AA quality and inversely sharpness. + // Where N ranges between, + // N = 0.50 (default) + // N = 0.33 (sharper) + // {x___} = -N/screenWidthInPixels + // {_y__} = -N/screenHeightInPixels + // {__z_} = N/screenWidthInPixels + // {___w} = N/screenHeightInPixels + FxaaFloat4 fxaaConsoleRcpFrameOpt, + // + // Only used on FXAA Console. + // Not used on 360, but used on PS3 and PC. + // This must be from a constant/uniform. + // {x___} = -2.0/screenWidthInPixels + // {_y__} = -2.0/screenHeightInPixels + // {__z_} = 2.0/screenWidthInPixels + // {___w} = 2.0/screenHeightInPixels + FxaaFloat4 fxaaConsoleRcpFrameOpt2, + // + // Only used on FXAA Console. + // Only used on 360 in place of fxaaConsoleRcpFrameOpt2. + // This must be from a constant/uniform. + // {x___} = 8.0/screenWidthInPixels + // {_y__} = 8.0/screenHeightInPixels + // {__z_} = -4.0/screenWidthInPixels + // {___w} = -4.0/screenHeightInPixels + FxaaFloat4 fxaaConsole360RcpFrameOpt2, + // + // Only used on FXAA Quality. + // This used to be the FXAA_QUALITY__SUBPIX define. + // It is here now to allow easier tuning. + // Choose the amount of sub-pixel aliasing removal. + // This can effect sharpness. + // 1.00 - upper limit (softer) + // 0.75 - default amount of filtering + // 0.50 - lower limit (sharper, less sub-pixel aliasing removal) + // 0.25 - almost off + // 0.00 - completely off + FxaaFloat fxaaQualitySubpix, + // + // Only used on FXAA Quality. + // This used to be the FXAA_QUALITY__EDGE_THRESHOLD define. + // It is here now to allow easier tuning. + // The minimum amount of local contrast required to apply algorithm. + // 0.333 - too little (faster) + // 0.250 - low quality + // 0.166 - default + // 0.125 - high quality + // 0.063 - overkill (slower) + FxaaFloat fxaaQualityEdgeThreshold, + // + // Only used on FXAA Quality. + // This used to be the FXAA_QUALITY__EDGE_THRESHOLD_MIN define. + // It is here now to allow easier tuning. + // Trims the algorithm from processing darks. + // 0.0833 - upper limit (default, the start of visible unfiltered edges) + // 0.0625 - high quality (faster) + // 0.0312 - visible limit (slower) + // Special notes when using FXAA_GREEN_AS_LUMA, + // Likely want to set this to zero. + // As colors that are mostly not-green + // will appear very dark in the green channel! + // Tune by looking at mostly non-green content, + // then start at zero and increase until aliasing is a problem. + FxaaFloat fxaaQualityEdgeThresholdMin, + // + // Only used on FXAA Console. + // This used to be the FXAA_CONSOLE__EDGE_SHARPNESS define. + // It is here now to allow easier tuning. + // This does not effect PS3, as this needs to be compiled in. + // Use FXAA_CONSOLE__PS3_EDGE_SHARPNESS for PS3. + // Due to the PS3 being ALU bound, + // there are only three safe values here: 2 and 4 and 8. + // These options use the shaders ability to a free *|/ by 2|4|8. + // For all other platforms can be a non-power of two. + // 8.0 is sharper (default!!!) + // 4.0 is softer + // 2.0 is really soft (good only for vector graphics inputs) + FxaaFloat fxaaConsoleEdgeSharpness, + // + // Only used on FXAA Console. + // This used to be the FXAA_CONSOLE__EDGE_THRESHOLD define. + // It is here now to allow easier tuning. + // This does not effect PS3, as this needs to be compiled in. + // Use FXAA_CONSOLE__PS3_EDGE_THRESHOLD for PS3. + // Due to the PS3 being ALU bound, + // there are only two safe values here: 1/4 and 1/8. + // These options use the shaders ability to a free *|/ by 2|4|8. + // The console setting has a different mapping than the quality setting. + // Other platforms can use other values. + // 0.125 leaves less aliasing, but is softer (default!!!) + // 0.25 leaves more aliasing, and is sharper + FxaaFloat fxaaConsoleEdgeThreshold, + // + // Only used on FXAA Console. + // This used to be the FXAA_CONSOLE__EDGE_THRESHOLD_MIN define. + // It is here now to allow easier tuning. + // Trims the algorithm from processing darks. + // The console setting has a different mapping than the quality setting. + // This only applies when FXAA_EARLY_EXIT is 1. + // This does not apply to PS3, + // PS3 was simplified to avoid more shader instructions. + // 0.06 - faster but more aliasing in darks + // 0.05 - default + // 0.04 - slower and less aliasing in darks + // Special notes when using FXAA_GREEN_AS_LUMA, + // Likely want to set this to zero. + // As colors that are mostly not-green + // will appear very dark in the green channel! + // Tune by looking at mostly non-green content, + // then start at zero and increase until aliasing is a problem. + FxaaFloat fxaaConsoleEdgeThresholdMin, + // + // Extra constants for 360 FXAA Console only. + // Use zeros or anything else for other platforms. + // These must be in physical constant registers and NOT immedates. + // Immedates will result in compiler un-optimizing. + // {xyzw} = float4(1.0, -1.0, 0.25, -0.25) + FxaaFloat4 fxaaConsole360ConstDir +) { +/*--------------------------------------------------------------------------*/ + FxaaFloat2 posM; + posM.x = pos.x; + posM.y = pos.y; + #if (FXAA_GATHER4_ALPHA == 1) + #if (FXAA_DISCARD == 0) + FxaaFloat4 rgbyM = FxaaTexTop(tex, posM); + #if (FXAA_GREEN_AS_LUMA == 0) + #define lumaM rgbyM.w + #else + #define lumaM rgbyM.y + #endif + #endif + #if (FXAA_GREEN_AS_LUMA == 0) + FxaaFloat4 luma4A = FxaaTexAlpha4(tex, posM); + FxaaFloat4 luma4B = FxaaTexOffAlpha4(tex, posM, FxaaInt2(-1, -1)); + #else + FxaaFloat4 luma4A = FxaaTexGreen4(tex, posM); + FxaaFloat4 luma4B = FxaaTexOffGreen4(tex, posM, FxaaInt2(-1, -1)); + #endif + #if (FXAA_DISCARD == 1) + #define lumaM luma4A.w + #endif + #define lumaE luma4A.z + #define lumaS luma4A.x + #define lumaSE luma4A.y + #define lumaNW luma4B.w + #define lumaN luma4B.z + #define lumaW luma4B.x + #else + FxaaFloat4 rgbyM = FxaaTexTop(tex, posM); + #if (FXAA_GREEN_AS_LUMA == 0) + #define lumaM rgbyM.w + #else + #define lumaM rgbyM.y + #endif + FxaaFloat lumaS = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2( 0, 1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaE = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2( 1, 0), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaN = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2( 0,-1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaW = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(-1, 0), fxaaQualityRcpFrame.xy)); + #endif +/*--------------------------------------------------------------------------*/ + FxaaFloat maxSM = max(lumaS, lumaM); + FxaaFloat minSM = min(lumaS, lumaM); + FxaaFloat maxESM = max(lumaE, maxSM); + FxaaFloat minESM = min(lumaE, minSM); + FxaaFloat maxWN = max(lumaN, lumaW); + FxaaFloat minWN = min(lumaN, lumaW); + FxaaFloat rangeMax = max(maxWN, maxESM); + FxaaFloat rangeMin = min(minWN, minESM); + FxaaFloat rangeMaxScaled = rangeMax * fxaaQualityEdgeThreshold; + FxaaFloat range = rangeMax - rangeMin; + FxaaFloat rangeMaxClamped = max(fxaaQualityEdgeThresholdMin, rangeMaxScaled); + FxaaBool earlyExit = range < rangeMaxClamped; +/*--------------------------------------------------------------------------*/ + if(earlyExit) + #if (FXAA_DISCARD == 1) + FxaaDiscard; + #else + return rgbyM; + #endif +/*--------------------------------------------------------------------------*/ + #if (FXAA_GATHER4_ALPHA == 0) + FxaaFloat lumaNW = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(-1,-1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaSE = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2( 1, 1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaNE = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2( 1,-1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaSW = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(-1, 1), fxaaQualityRcpFrame.xy)); + #else + FxaaFloat lumaNE = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(1, -1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaSW = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(-1, 1), fxaaQualityRcpFrame.xy)); + #endif +/*--------------------------------------------------------------------------*/ + FxaaFloat lumaNS = lumaN + lumaS; + FxaaFloat lumaWE = lumaW + lumaE; + FxaaFloat subpixRcpRange = 1.0/range; + FxaaFloat subpixNSWE = lumaNS + lumaWE; + FxaaFloat edgeHorz1 = (-2.0 * lumaM) + lumaNS; + FxaaFloat edgeVert1 = (-2.0 * lumaM) + lumaWE; +/*--------------------------------------------------------------------------*/ + FxaaFloat lumaNESE = lumaNE + lumaSE; + FxaaFloat lumaNWNE = lumaNW + lumaNE; + FxaaFloat edgeHorz2 = (-2.0 * lumaE) + lumaNESE; + FxaaFloat edgeVert2 = (-2.0 * lumaN) + lumaNWNE; +/*--------------------------------------------------------------------------*/ + FxaaFloat lumaNWSW = lumaNW + lumaSW; + FxaaFloat lumaSWSE = lumaSW + lumaSE; + FxaaFloat edgeHorz4 = (abs(edgeHorz1) * 2.0) + abs(edgeHorz2); + FxaaFloat edgeVert4 = (abs(edgeVert1) * 2.0) + abs(edgeVert2); + FxaaFloat edgeHorz3 = (-2.0 * lumaW) + lumaNWSW; + FxaaFloat edgeVert3 = (-2.0 * lumaS) + lumaSWSE; + FxaaFloat edgeHorz = abs(edgeHorz3) + edgeHorz4; + FxaaFloat edgeVert = abs(edgeVert3) + edgeVert4; +/*--------------------------------------------------------------------------*/ + FxaaFloat subpixNWSWNESE = lumaNWSW + lumaNESE; + FxaaFloat lengthSign = fxaaQualityRcpFrame.x; + FxaaBool horzSpan = edgeHorz >= edgeVert; + FxaaFloat subpixA = subpixNSWE * 2.0 + subpixNWSWNESE; +/*--------------------------------------------------------------------------*/ + if(!horzSpan) lumaN = lumaW; + if(!horzSpan) lumaS = lumaE; + if(horzSpan) lengthSign = fxaaQualityRcpFrame.y; + FxaaFloat subpixB = (subpixA * (1.0/12.0)) - lumaM; +/*--------------------------------------------------------------------------*/ + FxaaFloat gradientN = lumaN - lumaM; + FxaaFloat gradientS = lumaS - lumaM; + FxaaFloat lumaNN = lumaN + lumaM; + FxaaFloat lumaSS = lumaS + lumaM; + FxaaBool pairN = abs(gradientN) >= abs(gradientS); + FxaaFloat gradient = max(abs(gradientN), abs(gradientS)); + if(pairN) lengthSign = -lengthSign; + FxaaFloat subpixC = FxaaSat(abs(subpixB) * subpixRcpRange); +/*--------------------------------------------------------------------------*/ + FxaaFloat2 posB; + posB.x = posM.x; + posB.y = posM.y; + FxaaFloat2 offNP; + offNP.x = (!horzSpan) ? 0.0 : fxaaQualityRcpFrame.x; + offNP.y = ( horzSpan) ? 0.0 : fxaaQualityRcpFrame.y; + if(!horzSpan) posB.x += lengthSign * 0.5; + if( horzSpan) posB.y += lengthSign * 0.5; +/*--------------------------------------------------------------------------*/ + FxaaFloat2 posN; + posN.x = posB.x - offNP.x * FXAA_QUALITY__P0; + posN.y = posB.y - offNP.y * FXAA_QUALITY__P0; + FxaaFloat2 posP; + posP.x = posB.x + offNP.x * FXAA_QUALITY__P0; + posP.y = posB.y + offNP.y * FXAA_QUALITY__P0; + FxaaFloat subpixD = ((-2.0)*subpixC) + 3.0; + FxaaFloat lumaEndN = FxaaLuma(FxaaTexTop(tex, posN)); + FxaaFloat subpixE = subpixC * subpixC; + FxaaFloat lumaEndP = FxaaLuma(FxaaTexTop(tex, posP)); +/*--------------------------------------------------------------------------*/ + if(!pairN) lumaNN = lumaSS; + FxaaFloat gradientScaled = gradient * 1.0/4.0; + FxaaFloat lumaMM = lumaM - lumaNN * 0.5; + FxaaFloat subpixF = subpixD * subpixE; + FxaaBool lumaMLTZero = lumaMM < 0.0; +/*--------------------------------------------------------------------------*/ + lumaEndN -= lumaNN * 0.5; + lumaEndP -= lumaNN * 0.5; + FxaaBool doneN = abs(lumaEndN) >= gradientScaled; + FxaaBool doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P1; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P1; + FxaaBool doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P1; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P1; +/*--------------------------------------------------------------------------*/ + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P2; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P2; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P2; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P2; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 3) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P3; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P3; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P3; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P3; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 4) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P4; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P4; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P4; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P4; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 5) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P5; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P5; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P5; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P5; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 6) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P6; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P6; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P6; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P6; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 7) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P7; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P7; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P7; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P7; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 8) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P8; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P8; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P8; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P8; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 9) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P9; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P9; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P9; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P9; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 10) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P10; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P10; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P10; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P10; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 11) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P11; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P11; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P11; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P11; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 12) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P12; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P12; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P12; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P12; +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } +/*--------------------------------------------------------------------------*/ + FxaaFloat dstN = posM.x - posN.x; + FxaaFloat dstP = posP.x - posM.x; + if(!horzSpan) dstN = posM.y - posN.y; + if(!horzSpan) dstP = posP.y - posM.y; +/*--------------------------------------------------------------------------*/ + FxaaBool goodSpanN = (lumaEndN < 0.0) != lumaMLTZero; + FxaaFloat spanLength = (dstP + dstN); + FxaaBool goodSpanP = (lumaEndP < 0.0) != lumaMLTZero; + FxaaFloat spanLengthRcp = 1.0/spanLength; +/*--------------------------------------------------------------------------*/ + FxaaBool directionN = dstN < dstP; + FxaaFloat dst = min(dstN, dstP); + FxaaBool goodSpan = directionN ? goodSpanN : goodSpanP; + FxaaFloat subpixG = subpixF * subpixF; + FxaaFloat pixelOffset = (dst * (-spanLengthRcp)) + 0.5; + FxaaFloat subpixH = subpixG * fxaaQualitySubpix; +/*--------------------------------------------------------------------------*/ + FxaaFloat pixelOffsetGood = goodSpan ? pixelOffset : 0.0; + FxaaFloat pixelOffsetSubpix = max(pixelOffsetGood, subpixH); + if(!horzSpan) posM.x += pixelOffsetSubpix * lengthSign; + if( horzSpan) posM.y += pixelOffsetSubpix * lengthSign; + #if (FXAA_DISCARD == 1) + return FxaaTexTop(tex, posM); + #else + return FxaaFloat4(FxaaTexTop(tex, posM).xyz, lumaM); + #endif +} +/*==========================================================================*/ +#endif + + + + +//---------------------------------------------------------------------------------- +// File: es3-kepler/FXAA/assets/shaders/FXAA_Extreme_Quality.frag +// SDK Version: v2.11 +// Email: gameworks@nvidia.com +// Site: http://developer.nvidia.com/ +// +// Copyright (c) 2014-2015, NVIDIA CORPORATION. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of NVIDIA CORPORATION nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +//---------------------------------------------------------------------------------- + +precision highp float; + +uniform sampler2D color_texture; +uniform vec2 RCPFrame; +in vec2 frag_uv; + +out vec4 fragment_color; + + +void main(void) +{ + // fragment_color = texture(color_texture, frag_uv); + fragment_color.rgb = FxaaPixelShader( + frag_uv, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsolePosPos, + color_texture, // FxaaTex tex, + color_texture, // FxaaTex fxaaConsole360TexExpBiasNegOne, + color_texture, // FxaaTex fxaaConsole360TexExpBiasNegTwo, + RCPFrame, // FxaaFloat2 fxaaQualityRcpFrame, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt2, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsole360RcpFrameOpt2, + 0.75f, // FxaaFloat fxaaQualitySubpix, + 0.166f, // FxaaFloat fxaaQualityEdgeThreshold, + 0.0833f, // FxaaFloat fxaaQualityEdgeThresholdMin, + 0.0f, // FxaaFloat fxaaConsoleEdgeSharpness, + 0.0f, // FxaaFloat fxaaConsoleEdgeThreshold, + 0.0f, // FxaaFloat fxaaConsoleEdgeThresholdMin, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f) // FxaaFloat fxaaConsole360ConstDir, + ).rgb; +} diff --git a/GLMakie/assets/shader/postprocessing/postprocess.frag b/GLMakie/assets/shader/postprocessing/postprocess.frag new file mode 100644 index 00000000000..fbf420ede90 --- /dev/null +++ b/GLMakie/assets/shader/postprocessing/postprocess.frag @@ -0,0 +1,27 @@ +{{GLSL_VERSION}} + +in vec2 frag_uv; + +uniform sampler2D color_texture; + +layout(location=0) out vec4 fragment_color; + +vec3 linear_tone_mapping(vec3 color, float gamma) +{ + color = clamp(color, 0., 1.); + color = pow(color, vec3(1. / gamma)); + return color; +} + +void main(void) +{ + vec4 color = texture(color_texture, frag_uv).rgba; + if(color.a <= 0){ + discard; + } + // do tonemapping + //opaque = linear_tone_mapping(color.rgb, 1.8); // linear color output + fragment_color.rgb = color.rgb; + // save luma in alpha for FXAA + fragment_color.a = dot(color.rgb, vec3(0.299, 0.587, 0.114)); // compute luma +} diff --git a/GLMakie/assets/shader/sprites.geom b/GLMakie/assets/shader/sprites.geom new file mode 100644 index 00000000000..1a7c028f590 --- /dev/null +++ b/GLMakie/assets/shader/sprites.geom @@ -0,0 +1,187 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +// Half width of antialiasing smoothstep. NB: Should match fragment shader +#define ANTIALIAS_RADIUS 0.8 + +struct Nothing{ bool _; }; + +layout(points) in; +layout(triangle_strip, max_vertices = 4) out; + +mat4 qmat(vec4 quat){ + float num = quat.x * 2.0; + float num2 = quat.y * 2.0; + float num3 = quat.z * 2.0; + float num4 = quat.x * num; + float num5 = quat.y * num2; + float num6 = quat.z * num3; + float num7 = quat.x * num2; + float num8 = quat.x * num3; + float num9 = quat.y * num3; + float num10 = quat.w * num; + float num11 = quat.w * num2; + float num12 = quat.w * num3; + return mat4( + (1.0 - (num5 + num6)), (num7 + num12), (num8 - num11), 0.0, + (num7 - num12), (1.0 - (num4 + num6)), (num9 + num10), 0.0, + (num8 + num11), (num9 - num10), (1.0 - (num4 + num5)), 0.0, + 0.0, 0.0, 0.0, 1.0 + ); +} + +{{distancefield_type}} distancefield; + +uniform bool scale_primitive; +uniform bool billboard; +uniform float stroke_width; +uniform float glow_width; +uniform int shape; // for RECTANGLE hack below +uniform vec2 resolution; + +in int g_primitive_index[]; +in vec4 g_uv_texture_bbox[]; +in vec4 g_color[]; +in vec4 g_stroke_color[]; +in vec4 g_glow_color[]; +in vec3 g_position[]; +in vec4 g_rotation[]; +in vec4 g_offset_width[]; +in uvec2 g_id[]; + +flat out int f_primitive_index; +flat out float f_viewport_from_u_scale; +flat out float f_distancefield_scale; +flat out vec4 f_color; +flat out vec4 f_bg_color; +flat out vec4 f_stroke_color; +flat out vec4 f_glow_color; +flat out uvec2 f_id; +out vec2 f_uv; +flat out vec4 f_uv_texture_bbox; + +uniform bool use_pixel_marker; +uniform mat4 projection, view, model, pixel_space; + +float get_distancefield_scale(sampler2D distancefield){ + // Glyph distance field units are in pixels; convert to dimensionless + // x-coordinate of texture instead for consistency with programmatic uv + // distance fields in fragment shader. See also comments below. + float pixsize_x = (g_uv_texture_bbox[0].z - g_uv_texture_bbox[0].x) * + textureSize(distancefield, 0).x; + return -1.0/pixsize_x; +} + +float get_distancefield_scale(Nothing distancefield){ + return 1.0; +} + +void emit_vertex(vec4 vertex, vec2 uv) +{ + gl_Position = vertex; + f_uv = 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]; + EmitVertex(); +} + + +mat2 diagm(vec2 v){ + return mat2(v.x, 0.0, 0.0, v.y); +} + +out vec4 o_view_pos; +out vec3 o_normal; + +void main(void) +{ + o_view_pos = vec4(0); + o_normal = vec3(0); + + // emit quad as triangle strip + // v3. ____ . v4 + // |\ | + // | \ | + // | \ | + // |___\| + // v1* * v2 + // Centred bounding box of billboard + vec4 o_w = g_offset_width[0]; + vec2 bbox_signed_radius = 0.5*o_w.zw; // note; components may be negative. + vec2 sprite_bbox_centre = o_w.xy + bbox_signed_radius; + + mat4 pview = projection * view; + // Compute transform for the offset vectors from the central point + mat4 trans = scale_primitive ? model : mat4(1.0); + mat4 billtrans = use_pixel_marker ? pixel_space : projection; + trans = (billboard ? billtrans : pview) * qmat(g_rotation[0]) * trans; + + // Compute centre of billboard in clipping coordinates + vec4 vclip = pview*model*vec4(g_position[0],1) + trans*vec4(sprite_bbox_centre,0,0); + + // Extra buffering is required around sprites which are antialiased so that + // the antialias blur doesn't get cut off (see #15). This blur falls to + // zero at a radius of ANTIALIAS_RADIUS pixels in the viewport coordinates + // and we want to buffer the vertices in the *source* sprite coordinate + // system so that we get this amount in the output coordinates. + // + // Here we calculate the derivative of the mapping from sprite xy + // coordinates (defined by `trans`) into the viewport pixel coordinates. + // The derivative needs to include the proper term for the perspective + // divide into NDC, evaluated at the centre point `vclip`. + mat4 d_ndc_d_clip = mat4(1.0/vclip.w, 0.0, 0.0, 0.0, + 0.0, 1.0/vclip.w, 0.0, 0.0, + 0.0, 0.0, 1.0/vclip.w, 0.0, + -vclip.xyz/(vclip.w*vclip.w), 0.0); + mat2 dxyv_dxys = diagm(0.5*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 + // transformations, the geometric mean of the two principle scale factors + // is a reasonable compromise: + float viewport_from_sprite_scale = sqrt(abs(determinant(dxyv_dxys))); + + // In the fragment shader we want our signed distance in viewport (pixel) + // coords for direct use in antialiasing step functions. We therefore need + // a scaling factor similar to viewport_from_sprite_scale, but including + // the uv->sprite coordinate system scaling factor as well. We choose to + // use the bounding box *x* width for this. This comes with some + // consistency conditions: + // * For procedural distance fields, we need the sprite bounding box to be + // square. (If not, the uv coordinates will be anisotropically scaled and + // any calculation based on them will not be a distance function.) + // * For sampled distance fields, we need to consistently choose the *x* + // for the scaling in get_distancefield_scale(). + float sprite_from_u_scale = abs(o_w.z); + f_viewport_from_u_scale = viewport_from_sprite_scale * sprite_from_u_scale; + f_distancefield_scale = get_distancefield_scale(distancefield); + + // Compute required amount of buffering + float sprite_from_viewport_scale = 1.0 / viewport_from_sprite_scale; + float bbox_buf = sprite_from_viewport_scale * + (// Hack!! antialiasing is disabled for RECTANGLE==1 for now + // because it's used for boxplots where the sprites are + // long and skinny (violating assumption 1 above) + (shape == 1 ? 0.0 : ANTIALIAS_RADIUS) + + max(glow_width, 0) + max(stroke_width, 0)); + // Compute xy bounding box of billboard (in model space units) after + // buffering and associated bounding box of uv coordinates. + vec2 bbox_radius_buf = bbox_signed_radius + sign(bbox_signed_radius)*bbox_buf; + vec4 bbox = vec4(-bbox_radius_buf, bbox_radius_buf); + // uv bounding box is the buffered version of the domain [0,1]x[0,1] + vec2 uv_radius = 0.5 * bbox_radius_buf / bbox_signed_radius; + vec2 uv_center = vec2(0.5); + vec4 uv_bbox = vec4(uv_center-uv_radius, uv_center+uv_radius); //minx, miny, maxx, maxy + + emit_vertex(vclip + trans*vec4(bbox.xy,0,0), uv_bbox.xw); + emit_vertex(vclip + trans*vec4(bbox.xw,0,0), uv_bbox.xy); + emit_vertex(vclip + trans*vec4(bbox.zy,0,0), uv_bbox.zw); + emit_vertex(vclip + trans*vec4(bbox.zw,0,0), uv_bbox.zy); + + EndPrimitive(); +} diff --git a/GLMakie/assets/shader/sprites.vert b/GLMakie/assets/shader/sprites.vert new file mode 100644 index 00000000000..4caf0a4e1f7 --- /dev/null +++ b/GLMakie/assets/shader/sprites.vert @@ -0,0 +1,129 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +}; +struct Grid1D{ + int lendiv; + float start; + float stop; + int dims; +}; +struct Grid2D{ + ivec2 lendiv; + vec2 start; + vec2 stop; + ivec2 dims; +}; +struct Grid3D{ + ivec3 lendiv; + vec3 start; + vec3 stop; + ivec3 dims; +}; + +{{uv_offset_width_type}} uv_offset_width; +//{{uv_x_type}} uv_width; +{{position_type}} position; +{{position_x_type}} position_x; +{{position_y_type}} position_y; +{{position_z_type}} position_z; +//Assembling functions for creating the right position from the above inputs. They also indicate the type combinations allowed for the above inputs +ivec2 ind2sub(ivec2 dim, int linearindex); +ivec3 ind2sub(ivec3 dim, int linearindex); + +{{scale_type}} scale; // so in the case of distinct x,y,z, there's no chance to unify them under one variable +{{scale_x_type}} scale_x; +{{scale_y_type}} scale_y; +{{scale_z_type}} scale_z; +vec3 _scale(Nothing scale, Nothing scale_x, Nothing scale_y, Nothing scale_z, int index); +vec3 _scale(vec3 scale, Nothing scale_x, Nothing scale_y, Nothing scale_z, int index); +vec3 _scale(vec2 scale, Nothing scale_x, Nothing scale_y, Nothing scale_z, int index); +vec3 _scale(Nothing scale, float scale_x, float scale_y, float scale_z, int index); +vec3 _scale(vec3 scale, float scale_x, float scale_y, float scale_z, int index); +vec3 _scale(vec2 scale, float scale_x, float scale_y, float scale_z, int index); + + + +{{offset_type}} offset; + +{{rotation_type}} rotation; + +vec4 _rotation(Nothing r){return vec4(0,0,0,1);} +vec4 _rotation(vec2 r){return vec4(r, 0, 1);} +vec4 _rotation(vec3 r){return vec4(r, 1);} +vec4 _rotation(vec4 r){return r;} + +float get_rotation_len(Nothing rotation){ + return 1.0; +} + +float get_rotation_len(vec4 rotation){ + return 1.0; +} + +vec3 _scale(Nothing scale, float scale_x, float scale_y, Nothing scale_z, int index){ + float len = get_rotation_len(rotation); + return vec3(scale_x,scale_y, len); +} +vec3 _scale(vec3 scale, Nothing scale_x, Nothing scale_y, Nothing scale_z, int index){ + float len = get_rotation_len(rotation); + return vec3(scale.xy, scale.z*len); +} + +{{color_type}} color; +{{color_map_type}} color_map; +{{intensity_type}} intensity; +{{color_norm_type}} color_norm; + +float get_intensity(vec4 rotation, Nothing position_z, int index){return length(rotation);} +float get_intensity(vec3 rotation, Nothing position_z, int index){return length(rotation);} +float get_intensity(vec2 rotation, Nothing position_z, int index){return length(rotation);} +float get_intensity(Nothing rotation, float position_z, int index){return position_z;} +float get_intensity(vec3 rotation, float position_z, int index){return position_z;} +vec4 color_lookup(float intensity, sampler1D color_ramp, vec2 norm); + +vec4 _color(vec3 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len); +vec4 _color(vec4 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len); +vec4 _color(Nothing color, float intensity, sampler1D color_map, vec2 color_norm, int index, int len); +vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_norm, int index, int len); +vec4 _color(Nothing color, Nothing intensity, sampler1D color_map, vec2 color_norm, int index, int len){ + return color_lookup(get_intensity(rotation, position_z, index), color_map, color_norm); +} + +{{stroke_color_type}} stroke_color; +{{glow_color_type}} glow_color; + +uniform uint objectid; +uniform int len; + +out uvec2 g_id; +out int g_primitive_index; +out vec3 g_position; +out vec4 g_offset_width; +out vec4 g_uv_texture_bbox; +out vec4 g_rotation; +out vec4 g_color; +out vec4 g_stroke_color; +out vec4 g_glow_color; + +vec4 to_vec4(vec3 x){return vec4(x, 1.0);} +vec4 to_vec4(vec4 x){return x;} + +void main(){ + int index = gl_VertexID; + g_primitive_index = index; + vec3 pos; + {{position_calc}} + g_position = pos; + g_offset_width.xy = offset.xy; + g_offset_width.zw = _scale(scale, scale_x, scale_y, scale_z, g_primitive_index).xy; + g_color = _color(color, intensity, color_map, color_norm, g_primitive_index, len); + g_rotation = _rotation(rotation); + g_uv_texture_bbox = uv_offset_width; + g_stroke_color = to_vec4(stroke_color); + g_glow_color = to_vec4(glow_color); + + g_id = uvec2(objectid, index+1); +} diff --git a/GLMakie/assets/shader/standard.frag b/GLMakie/assets/shader/standard.frag new file mode 100644 index 00000000000..88d1d0f88b5 --- /dev/null +++ b/GLMakie/assets/shader/standard.frag @@ -0,0 +1,104 @@ +{{GLSL_VERSION}} + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +}; + +uniform vec3 ambient; +uniform vec3 diffuse; +uniform vec3 specular; +uniform float shininess; +uniform float backlight; + +in vec3 o_normal; +in vec3 o_lightdir; +in vec3 o_camdir; +in vec4 o_color; +in vec2 o_uv; +flat in uvec2 o_id; + +{{matcap_type}} matcap; +{{image_type}} image; +{{color_map_type}} color_map; +{{color_norm_type}} color_norm; + +vec4 get_color(Nothing image, vec2 uv, Nothing color_norm, Nothing color_map, Nothing matcap){ + return o_color; +} +vec4 get_color(Nothing color, vec2 uv, vec2 color_norm, sampler1D color_map, Nothing matcap){ + return o_color; +} + +vec4 get_color(sampler2D color, vec2 uv, vec2 color_norm, sampler1D color_map, Nothing matcap){ + return o_color; +} + +vec4 get_color(sampler2D color, vec2 uv, Nothing color_norm, Nothing color_map, Nothing matcap){ + return texture(color, uv); +} + +vec4 matcap_color(sampler2D matcap){ + vec2 muv = o_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){ + return matcap_color(matcap); +} +vec4 get_color(sampler2D color, vec2 uv, Nothing color_norm, Nothing color_map, sampler2D matcap){ + return matcap_color(matcap); +} +vec4 get_color(sampler1D color, vec2 uv, vec2 color_norm, sampler1D color_map, sampler2D matcap){ + return matcap_color(matcap); +} + +uniform bool fetch_pixel; +uniform vec2 uv_scale; + +vec4 get_pattern_color(sampler1D color) { + int size = textureSize(color, 0); + vec2 pos = gl_FragCoord.xy * uv_scale; + int idx = int(mod(pos.x, size)); + return texelFetch(color, idx, 0); +} + +vec4 get_pattern_color(sampler2D color){ + ivec2 size = textureSize(color, 0); + vec2 pos = gl_FragCoord.xy * uv_scale; + return texelFetch(color, ivec2(mod(pos.x, size.x), mod(pos.y, size.y)), 0); +} + +// 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); + + +void main(){ + vec4 color; + // Should this be a mustache replace? + if (fetch_pixel){ + color = get_pattern_color(image); + }else{ + color = get_color(image, o_uv, color_norm, color_map, matcap); + } + {{light_calc}} + write2framebuffer(color, o_id); +} diff --git a/GLMakie/assets/shader/standard.vert b/GLMakie/assets/shader/standard.vert new file mode 100644 index 00000000000..f7a2e2faf04 --- /dev/null +++ b/GLMakie/assets/shader/standard.vert @@ -0,0 +1,54 @@ +{{GLSL_VERSION}} + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +}; + +{{vertices_type}} vertices; +{{vertex_color_type}} vertex_color; +{{texturecoordinates_type}} texturecoordinates; + +{{color_map_type}} color_map; +{{color_norm_type}} color_norm; + +in vec3 normals; + +uniform vec3 lightposition; +uniform mat4 projection, view, model; +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition); + +uniform uint objectid; +flat out uvec2 o_id; +uniform vec2 uv_scale; +out vec2 o_uv; +out vec4 o_color; + +vec3 to_3d(vec2 v){return vec3(v, 0);} +vec3 to_3d(vec3 v){return v;} + +vec2 to_2d(float v){return vec2(v, 0);} +vec2 to_2d(vec2 v){return v;} + +vec4 to_color(vec3 c, Nothing color_map, Nothing color_norm){ + return vec4(c, 1); +} + +vec4 to_color(vec4 c, Nothing color_map, Nothing color_norm){ + return c; +} + +vec4 color_lookup(float intensity, sampler1D color_ramp, vec2 norm); + +vec4 to_color(float c, sampler1D color_map, vec2 color_norm){ + return color_lookup(c, color_map, color_norm); +} + +void main() +{ + o_id = uvec2(objectid, gl_VertexID+1); + vec2 tex_uv = to_2d(texturecoordinates); + o_uv = vec2(1.0 - tex_uv.y, tex_uv.x) * uv_scale; + o_color = to_color(vertex_color, color_map, color_norm); + vec3 v = to_3d(vertices); + render(model * vec4(v, 1), normals, view, projection, lightposition); +} diff --git a/GLMakie/assets/shader/surface.vert b/GLMakie/assets/shader/surface.vert new file mode 100644 index 00000000000..b042fab4b29 --- /dev/null +++ b/GLMakie/assets/shader/surface.vert @@ -0,0 +1,112 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +struct Grid2D{ + ivec2 lendiv; + vec2 start; + vec2 stop; + ivec2 dims; +}; + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +} nothing; + +in vec2 vertices; + +{{position_type}} position; +{{position_x_type}} position_x; +{{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; + +uniform vec4 highclip; +uniform vec4 lowclip; +uniform vec4 nan_color; + +vec4 color_lookup(float intensity, sampler1D color, vec2 norm); + +// constant color! +vec4 get_color(vec4 color, float _intensity, Nothing color_map, Nothing color_norm, vec2 index){ + return color; +} + +vec4 get_color(Nothing _, float i, sampler1D color_map, vec2 color_norm, ivec2 index){ + vec4 color = color_lookup(i, color_map, color_norm); + if (isnan(i)) { + color = nan_color; + } else if (i < color_norm.x) { + color = lowclip; + } else if (i > color_norm.y) { + color = highclip; + } + return color; +} + +vec4 get_color(sampler2D color, float _intensity, sampler1D color_map, vec2 color_norm, ivec2 index){ + float value = texelFetch(color, index, 0).r; + return get_color(Nothing(false), value, color_map, color_norm, index); // we fetch the color in fragment shader +} + +vec4 get_color(sampler2D color, float _intensity, Nothing b, Nothing c, ivec2 index){ + return vec4(0); // we fetch the color in fragment shader +} + +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); +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); + + +uniform bool wireframe; +uniform uint objectid; +uniform float stroke_width; +uniform vec2 uv_scale; +flat out uvec2 o_id; +out vec4 o_color; +out vec2 o_uv; + +flat out vec2 f_scale; +flat out vec4 f_color; +flat out vec4 f_bg_color; +flat out vec4 f_stroke_color; +flat out vec4 f_glow_color; +flat out int f_primitive_index; +flat out uvec2 f_id; + +out vec2 f_uv; +out vec2 f_uv_offset; + +void main() +{ + int index = gl_InstanceID; + vec2 offset = vertices; + ivec2 offseti = ivec2(offset); + ivec2 dims = textureSize(position_z, 0); + vec3 pos; + {{position_calc}} + o_color = get_color(image, pos.z, color_map, color_norm, index2D); + if (isnan(pos.z)) { + pos.z = 0.0; + } + o_id = uvec2(objectid, index1D+1); + o_uv = index01 * uv_scale; + vec3 normalvec = {{normal_calc}}; + render(model * vec4(pos, 1), normalvec, view, projection, lightposition); +} diff --git a/GLMakie/assets/shader/texture.frag b/GLMakie/assets/shader/texture.frag new file mode 100644 index 00000000000..aef5dc9f9b7 --- /dev/null +++ b/GLMakie/assets/shader/texture.frag @@ -0,0 +1,24 @@ +{{GLSL_VERSION}} + +in vec2 o_uv; +flat in uvec2 o_objectid; +out vec4 fragment_color; +out uvec2 fragment_groupid; + +{{image_type}} image; + +vec4 getindex(sampler2D image, vec2 uv){ + return texture(image, uv); +} +vec4 getindex(sampler1D image, vec2 uv){ + return texture(image, uv.x); +} + +void write2framebuffer(vec4 color, uvec2 id); + +void main(){ + write2framebuffer( + getindex(image, vec2(o_uv.x, 1-o_uv.y)), + o_objectid + ); +} diff --git a/GLMakie/assets/shader/util.vert b/GLMakie/assets/shader/util.vert new file mode 100644 index 00000000000..7689ffc70d5 --- /dev/null +++ b/GLMakie/assets/shader/util.vert @@ -0,0 +1,371 @@ +{{GLSL_VERSION}} + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +}; +struct Grid1D{ + int lendiv; + float start; + float stop; + int dims; +}; +struct Grid2D{ + ivec2 lendiv; + vec2 start; + vec2 stop; + ivec2 dims; +}; +struct Grid3D{ + ivec3 lendiv; + vec3 start; + vec3 stop; + ivec3 dims; +}; + +vec2 grid_pos(Grid2D position, vec2 uv){ + return vec2( + (1-uv[0]) * position.start[0] + uv[0] * position.stop[0], + (1-uv[1]) * position.start[1] + uv[1] * position.stop[1] + ); +} + + +// stretch is +vec3 stretch(vec3 val, vec3 from, vec3 to){ + return from + (val * (to - from)); +} +vec2 stretch(vec2 val, vec2 from, vec2 to){ + return from + (val * (to - from)); +} +float stretch(float val, float from, float to){ + return from + (val * (to - from)); +} + +float _normalize(float val, float from, float to){return (val-from) / (to - from);} +vec2 _normalize(vec2 val, vec2 from, vec2 to){ + return (val-from) / (to - from); +} +vec3 _normalize(vec3 val, vec3 from, vec3 to){ + return (val-from) / (to - from); +} + + +mat4 getmodelmatrix(vec3 xyz, vec3 scale){ + return mat4( + vec4(scale.x, 0, 0, 0), + vec4(0, scale.y, 0, 0), + vec4(0, 0, scale.z, 0), + vec4(xyz, 1)); +} + +mat4 rotationmatrix_z(float angle){ + return mat4( + cos(angle), -sin(angle), 0, 0, + sin(angle), cos(angle), 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); +} +mat4 rotationmatrix_y(float angle){ + return mat4( + cos(angle), 0, sin(angle), 0, + 0, 1, 0, 0, + -sin(angle), 0, cos(angle), 0, + 0, 0, 0, 1); +} + +vec3 qmul(vec4 quat, vec3 vec){ + float num = quat.x * 2.0; + float num2 = quat.y * 2.0; + float num3 = quat.z * 2.0; + float num4 = quat.x * num; + float num5 = quat.y * num2; + float num6 = quat.z * num3; + float num7 = quat.x * num2; + float num8 = quat.x * num3; + float num9 = quat.y * num3; + float num10 = quat.w * num; + float num11 = quat.w * num2; + float num12 = quat.w * num3; + return vec3( + (1.0 - (num5 + num6)) * vec.x + (num7 - num12) * vec.y + (num8 + num11) * vec.z, + (num7 + num12) * vec.x + (1.0 - (num4 + num6)) * vec.y + (num9 - num10) * vec.z, + (num8 - num11) * vec.x + (num9 + num10) * vec.y + (1.0 - (num4 + num5)) * vec.z + ); +} + + +void rotate(Nothing r, int index, inout vec3 V, inout vec3 N){} // no-op +void rotate(vec4 q, int index, inout vec3 V, inout vec3 N){ + V = qmul(q, V); + N = normalize(qmul(q, N)); +} +void rotate(samplerBuffer vectors, int index, inout vec3 V, inout vec3 N){ + vec4 r = texelFetch(vectors, index); + rotate(r, index, V, N); +} + + + +mat4 translate_scale(vec3 xyz, vec3 scale){ + return mat4( + vec4(scale.x, 0, 0, 0), + vec4(0, scale.y, 0, 0), + vec4(0, 0, scale.z, 0), + vec4(xyz, 1)); +} + +//Mapping 1D index to 1D, 2D and 3D arrays +int ind2sub(int dim, int linearindex){return linearindex;} +ivec2 ind2sub(ivec2 dim, int linearindex){ + return ivec2(linearindex % dim.x, linearindex / dim.x); +} +ivec3 ind2sub(ivec3 dim, int i){ + int z = i / (dim.x*dim.y); + i -= z * dim.x * dim.y; + return ivec3(i % dim.x, i / dim.x, z); +} + +float linear_index(int dims, int index){ + return float(index) / float(dims); +} +vec2 linear_index(ivec2 dims, int index){ + ivec2 index2D = ind2sub(dims, index); + return vec2(index2D) / vec2(dims); +} +vec2 linear_index(ivec2 dims, int index, vec2 offset){ + vec2 index2D = vec2(ind2sub(dims, index))+offset; + return index2D / vec2(dims); +} +vec3 linear_index(ivec3 dims, int index){ + ivec3 index3D = ind2sub(dims, index); + return vec3(index3D) / vec3(dims); +} +vec4 linear_texture(sampler2D tex, int index){ + return texture(tex, linear_index(textureSize(tex, 0), index)); +} + +vec4 linear_texture(sampler2D tex, int index, vec2 offset){ + ivec2 dims = textureSize(tex, 0); + return texture(tex, linear_index(dims, index) + (offset/vec2(dims))); +} + +vec4 linear_texture(sampler3D tex, int index){ + return texture(tex, linear_index(textureSize(tex, 0), index)); +} +uvec4 getindex(usampler2D tex, int index){ + return texelFetch(tex, ind2sub(textureSize(tex, 0), index), 0); +} +vec4 getindex(samplerBuffer tex, int index){ + return texelFetch(tex, index); +} +vec4 getindex(sampler1D tex, int index){ + return texelFetch(tex, index, 0); +} +vec4 getindex(sampler2D tex, int index){ + return texelFetch(tex, ind2sub(textureSize(tex, 0), index), 0); +} +vec4 getindex(sampler3D tex, int index){ + return texelFetch(tex, ind2sub(textureSize(tex, 0), index), 0); +} + + + +//vec3 _scale(vec3 scale, Nothing scale_x, Nothing scale_y, Nothing scale_z, int index){return scale;} +vec3 _scale(vec2 scale, Nothing scale_x, Nothing scale_y, Nothing scale_z, int index){return vec3(scale,1);} +vec3 _scale(float scale, Nothing scale_x, Nothing scale_y, Nothing scale_z, int index){return vec3(scale);} +vec3 _scale(Nothing scale, float scale_x, float scale_y, float scale_z, int index){ + return vec3(scale_x, scale_y, scale_z); +} +vec3 _scale(vec2 scale, float scale_x, float scale_y, float scale_z, int index){ + return vec3(scale.x*scale_x, scale.y*scale_y, scale_z); +} +vec3 _scale(vec3 scale, float scale_x, float scale_y, float scale_z, int index){ + return vec3(scale_x, scale_y, scale_z)*scale; +} +vec3 _scale(samplerBuffer scale, Nothing scale_x, Nothing scale_y, Nothing scale_z, int index){ + return getindex(scale, index).xyz; +} +vec3 _scale(vec3 scale, float scale_x, float scale_y, samplerBuffer scale_z, int index){ + return vec3(scale_x, scale_y, getindex(scale_z, index).x); +} +vec3 _scale(Nothing scale, float scale_x, float scale_y, samplerBuffer scale_z, int index){ + return vec3(scale_x, scale_y, getindex(scale_z, index).x); +} +vec3 _scale(vec3 scale, float scale_x, samplerBuffer scale_y, float scale_z, int index){ + return vec3(scale_x, getindex(scale_y, index).x, scale_z); +} +vec3 _scale(Nothing scale, float scale_x, samplerBuffer scale_y, float scale_z, int index){ + return vec3(scale_x, getindex(scale_y, index).x, scale_z); +} + +vec4 color_lookup(float intensity, vec4 color, vec2 norm){ + return color; +} +vec4 color_lookup(float intensity, sampler1D color_ramp, vec2 norm){ + return texture(color_ramp, _normalize(intensity, norm.x, norm.y)); +} + +vec4 _color(vec3 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len){ + return vec4(color, 1); +} +vec4 _color(vec4 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len){return color;} +vec4 _color(samplerBuffer color, Nothing intensity, Nothing color_norm, int index){ + return texelFetch(color, index); +} +vec4 _color(samplerBuffer color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len){ + return texelFetch(color, index); +} +vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_norm, int index, int len){ + return color_lookup(texture(intensity, float(index)/float(len-1)).x, color_map, color_norm); +} +vec4 _color(Nothing color, samplerBuffer intensity, sampler1D color_map, vec2 color_norm, int index, int len){ + return color_lookup(texelFetch(intensity, index).x, color_map, color_norm); +} +vec4 _color(Nothing color, float intensity, sampler1D color_map, vec2 color_norm, int index, int len){ + return color_lookup(intensity, color_map, color_norm); +} + +out vec4 o_view_pos; +out vec3 o_normal; +out vec3 o_lightdir; +out vec3 o_camdir; +// transpose(inv(view * model)) +// Transformation for vectors (rather than points) +uniform mat3 normalmatrix; +uniform vec3 lightposition; +uniform vec3 eyeposition; + + +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition) +{ + // normal in world space + o_normal = normalmatrix * normal; + // position in view space (as seen from camera) + o_view_pos = view * position_world; + // position in clip space (w/ depth) + gl_Position = projection * o_view_pos; + // direction to light + o_lightdir = normalize(view*vec4(lightposition, 1.0) - o_view_pos).xyz; + // direction to camera + // This is equivalent to + // normalize(view*vec4(eyeposition, 1.0) - o_view_pos).xyz + // (by definition `view * eyeposition = 0`) + o_camdir = normalize(-o_view_pos).xyz; +} + +// +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(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); +} diff --git a/GLMakie/assets/shader/volume.frag b/GLMakie/assets/shader/volume.frag new file mode 100644 index 00000000000..14c4fb28ca0 --- /dev/null +++ b/GLMakie/assets/shader/volume.frag @@ -0,0 +1,331 @@ +{{GLSL_VERSION}} + +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; + +{{color_map_type}} color_map; +{{color_type}} color; +{{color_norm_type}} color_norm; + +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; +uniform float isorange; + +const float max_distance = 1.3; + +const int num_samples = 200; +const float step_size = max_distance / float(num_samples); + +float _normalize(float val, float from, float to) +{ + return (val-from) / (to - from); +} + +vec4 color_lookup(float intensity, Nothing color_map, Nothing norm, vec4 color) +{ + return color; +} + +vec4 color_lookup(float intensity, samplerBuffer color_ramp, vec2 norm, Nothing color) +{ + return texelFetch(color_ramp, int(_normalize(intensity, norm.x, norm.y)*textureSize(color_ramp))); +} + +vec4 color_lookup(float intensity, samplerBuffer color_ramp, Nothing norm, Nothing color) +{ + return vec4(0); // stub method +} + +vec4 color_lookup(float intensity, sampler1D color_ramp, vec2 norm, Nothing color) +{ + return texture(color_ramp, _normalize(intensity, norm.x, norm.y)); +} + +vec4 color_lookup(samplerBuffer colormap, int index) +{ + return texelFetch(colormap, index); +} + +vec4 color_lookup(sampler1D colormap, int index) +{ + return texelFetch(colormap, index, 0); +} + +vec4 color_lookup(Nothing colormap, int index) +{ + return vec4(0); +} + +vec3 gennormal(vec3 uvw, float d) +{ + vec3 a, b; + // handle normals at edges! + if(uvw.x + d >= 1.0){ + return vec3(1, 0, 0); + } + if(uvw.y + d >= 1.0){ + return vec3(0, 1, 0); + } + if(uvw.z + d >= 1.0){ + return vec3(0, 0, 1); + } + + if(uvw.x - d <= 0.0){ + return vec3(-1, 0, 0); + } + if(uvw.y - d <= 0.0){ + return vec3(0, -1, 0); + } + if(uvw.z - d <= 0.0){ + 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.y = texture(volumedata, uvw - vec3(0.0,d,0.0)).r; + b.y = texture(volumedata, uvw + vec3(0.0,d,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); +} + +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( + ambient * color + + diffuse * diff_coeff * color + + specular * spec_coeff + ); +} + +// Simple random generator found: http://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl +float rand(){ + return fract(sin(gl_FragCoord.x * 12.9898 + gl_FragCoord.y * 78.233) * 43758.5453); +} + +vec4 volume(vec3 front, vec3 dir) +{ + // The per-voxel alpha channel is specified in units of opacity/length. + // If our voxels are not isotropic, then the distance that we trace through + // depends on the direction. + vec3 pos = front; + float T = 1.0; + vec3 Lo = vec3(0.0); + int i = 0; + for (i; i < num_samples; ++i) { + float intensity = texture(volumedata, pos).x; + vec4 density = color_lookup(intensity, color_map, color_norm, color); + float opacity = step_size * density.a * absorption; + T *= 1.0-opacity; + if (T <= 0.01) + break; + + Lo += (T*opacity)*density.rgb; + pos += dir; + } + return vec4(Lo, 1-T); +} + +vec4 additivergba(vec3 front, vec3 dir) +{ + vec3 pos = front; + vec4 integrated_color = vec4(0., 0., 0., 0.); + int i = 0; + for (i; i < num_samples ; ++i) { + vec4 density = texture(volumedata, pos); + integrated_color = 1.0 - (1.0 - integrated_color) * (1.0 - density); + pos += dir; + } + return integrated_color; +} + +vec4 absorptionrgba(vec3 front, vec3 dir) +{ + vec3 pos = front; + float T = 1.0; + vec3 Lo = vec3(0.0); + int i = 0; + for (i; i < num_samples ; ++i) { + vec4 density = texture(volumedata, pos); + float opacity = step_size * density.a; + T *= 1.0-opacity; + if (T <= 0.01) + break; + + Lo += (T*opacity)*density.rgb; + pos += dir; + } + return vec4(Lo, 1-T); +} + +vec4 volumeindexedrgba(vec3 front, vec3 dir) +{ + vec3 pos = front; + float T = 1.0; + vec3 Lo = vec3(0.0); + int i = 0; + for (i; i < num_samples; ++i) { + int index = int(texture(volumedata, pos).x) - 1; + vec4 density = color_lookup(color_map, index); + float opacity = step_size*density.a; + Lo += (T*opacity)*density.rgb; + T *= 1.0 - opacity; + if (T <= 0.01) + break; + pos += dir; + } + return vec4(Lo, 1-T); +} + +vec4 contours(vec3 front, vec3 dir) +{ + vec3 pos = front; + float T = 1.0; + vec3 Lo = vec3(0.0); + int i = 0; + vec3 camdir = normalize(-dir); + for (i; i < num_samples; ++i) { + float intensity = texture(volumedata, pos).x; + vec4 density = color_lookup(intensity, color_map, color_norm, color); + float opacity = density.a; + if(opacity > 0.0){ + vec3 N = gennormal(pos, step_size); + vec3 L = normalize(o_light_dir - pos); + vec3 opaque = blinnphong(N, camdir, L, density.rgb); + Lo += (T * opacity) * opaque; + T *= 1.0 - opacity; + if (T <= 0.01) + break; + } + pos += dir; + } + return vec4(Lo, 1-T); +} + +vec4 isosurface(vec3 front, vec3 dir) +{ + vec3 pos = front; + vec4 c = vec4(0.0); + int i = 0; + vec4 diffuse_color = color_lookup(isovalue, color_map, color_norm, color); + 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); + // back & frontface... + vec3 c1 = blinnphong(N, camdir, L, diffuse_color.rgb); + vec3 c2 = blinnphong(-N, camdir, L, diffuse_color.rgb); + c = vec4(0.5*c1 + 0.5*c2, diffuse_color.a); + break; + } + pos += dir; + } + return c; +} + +vec4 mip(vec3 front, vec3 dir) +{ + vec3 pos = front; + int i = 0; + float maximum = 0.0; + for (i; i < num_samples; ++i, pos += dir){ + float density = texture(volumedata, pos).x; + if(maximum < density) + maximum = density; + } + return color_lookup(maximum, color_map, color_norm, color); +} + +uniform uint objectid; + +void write2framebuffer(vec4 color, uvec2 id); + +const float typemax = 100000000000000000000000000000000000000.0; + +bool no_solution(float x){ + return x <= 0.0001 || isinf(x) || isnan(x); +} + +float min_bigger_0(float a, float b){ + bool a_no = no_solution(a); + bool b_no = no_solution(b); + if(a_no && b_no){ + // no solution + return typemax; + } + if(a_no){ + return b; + } + if(b_no){ + return a; + } + return min(a, b); +} + +float min_bigger_0(vec3 v1, vec3 v2){ + float x = min_bigger_0(v1.x, v2.x); + float y = min_bigger_0(v1.y, v2.y); + float z = min_bigger_0(v1.z, v2.z); + return min(x, min(y, z)); +} + +void main() +{ + vec4 color; + vec3 eye_unit = vec3(modelinv * vec4(eyeposition, 1)); + vec3 back_position = vec3(modelinv * vec4(frag_vert, 1)); + vec3 dir = normalize(eye_unit - back_position); + // solve back_position + distance * dir == 1 + // solve back_position + distance * dir == 0 + // to see where it first hits unit cube! + vec3 solution_1 = (1.0 - back_position) / dir; + vec3 solution_0 = (0.0 - back_position) / dir; + float solution = min_bigger_0(solution_1, solution_0); + + vec3 start = back_position + solution * dir; + vec3 step_in_dir = (back_position - start) / num_samples; + + float steps = 0.1; + // the algorithm numbers correspond to the order in the + // RaymarchAlgorithm enum defined in Makie types.jl + if(algorithm == 0) + color = isosurface(start, step_in_dir); + else if(algorithm == 1) + color = volume(start, step_in_dir); + else if(algorithm == 2) + color = mip(start, step_in_dir); + else if(algorithm == 3) + color = absorptionrgba(start, step_in_dir); + else if(algorithm == 4) + color = additivergba(start, step_in_dir); + else if(algorithm == 5) + color = volumeindexedrgba(start, step_in_dir); + else + color = contours(start, step_in_dir); + + write2framebuffer(color, uvec2(objectid, 0)); +} diff --git a/GLMakie/assets/shader/volume.vert b/GLMakie/assets/shader/volume.vert new file mode 100644 index 00000000000..1ee93ca3df1 --- /dev/null +++ b/GLMakie/assets/shader/volume.vert @@ -0,0 +1,24 @@ +{{GLSL_VERSION}} + +in vec3 vertices; + +out vec3 frag_vert; +out vec3 o_light_dir; + +uniform mat4 projectionview, model; +uniform vec3 lightposition; +uniform mat4 modelinv; + +out vec4 o_view_pos; +out vec3 o_normal; + +void main() +{ + // TODO set these in volume.frag + o_view_pos = vec4(0); + o_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; +} diff --git a/GLMakie/experiments/cuda_interop.jl b/GLMakie/experiments/cuda_interop.jl new file mode 100644 index 00000000000..bcaf7df397a --- /dev/null +++ b/GLMakie/experiments/cuda_interop.jl @@ -0,0 +1,11 @@ +using CUDA, GLMakie + + +scene = scatter(rand(Point2f0, 10_000), show_axis=false) +screen = display(scene) + +buffer = screen.renderlist[1][3][:position] +resource = +cuGraphicsGLRegisterBuffer(&resource, pbo, cudaGraphicsRegisterFlagsReadOnly) + +CUDA.cuGraphicsMapResources diff --git a/GLMakie/experiments/mesh.frag b/GLMakie/experiments/mesh.frag new file mode 100644 index 00000000000..8822baf3fa0 --- /dev/null +++ b/GLMakie/experiments/mesh.frag @@ -0,0 +1,28 @@ + +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( + get_ambient() * color + + get_diffuse() * diff_coeff * color + + get_specular() * spec_coeff + ); +} + +void main() { + vec4 real_color = get_color(); + vec3 shaded_color = real_color.xyz; + + if(get_shading()){ + shaded_color = blinnphong(get_normal(), get_position(), get_lightposition(), real_color.xyz); + } + + fragment_color = vec4(shaded_color, real_color.a); +} diff --git a/GLMakie/experiments/mesh.vert b/GLMakie/experiments/mesh.vert new file mode 100644 index 00000000000..04592b1c389 --- /dev/null +++ b/GLMakie/experiments/mesh.vert @@ -0,0 +1,33 @@ +out vec2 frag_uv; +out vec3 frag_normal; +out vec3 frag_position; +out vec4 frag_color; +out vec3 frag_lightdir; + +uniform mat4 projectionMatrix; +uniform mat4 viewMatrix; +uniform mat4 modelMatrix; + +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;} + +void main(){ + // get_* gets the global inputs (uniform, sampler, vertex array) + // those functions will get inserted by the shader creation pipeline + vec3 position_object = tovec3(get_position()); + frag_normal = get_normals(); + frag_uv = get_texturecoordinates(); + + vec3 lightpos = get_lightposition(); + vec4 position_world = modelMatrix * vec4(position_object, 1); + frag_lightdir = normalize(lightpos - position_world.xyz); + // direction to camera + frag_position = -position_world.xyz; + frag_uv = vec2(1.0 - frag_uv.y, frag_uv.x); + frag_color = tovec4(get_color()); + // screen space coordinates of the vertex + gl_Position = projectionMatrix * viewMatrix * position_world; +} diff --git a/GLMakie/experiments/shaderabstr.jl b/GLMakie/experiments/shaderabstr.jl new file mode 100644 index 00000000000..af913e2c4da --- /dev/null +++ b/GLMakie/experiments/shaderabstr.jl @@ -0,0 +1,164 @@ +using ShaderAbstractions: Buffer, Sampler, VertexArray + +# Mesh +mesh(Sphere(Point3f0(0), 1f0)) |> display +mesh(Sphere(Point3f0(0), 1f0), color=:red, ambient=Vec3f0(0.9)) + +tocolor(x) = RGBf0(x...) +positions = Observable(decompose(Point3f0, Sphere(Point3f0(0), 1f0))) +triangles = Observable(decompose(GLTriangleFace, Sphere(Point3f0(0), 1f0))) +uv = Observable(GeometryBasics.decompose_uv(Sphere(Point3f0(0), 1f0))) +xyz_vertex_color = Observable(tocolor.(positions[])) +texture = Observable(rand(RGBAf0, 10, 10)) + +pos_buff = Buffer(positions) +triangles_buff = Buffer(triangles) +vert_color_buff = Buffer(xyz_vertex_color) +uv_buff = Buffer(uv) +texture_buff = Sampler(texture) +texsampler = Makie.sampler(:viridis, rand(length(positions))) + +coords = VertexArray(pos_buff, triangles_buff, color=vert_color_buff) +mesh = GeometryBasics.Mesh(coords) +GeometryBasics.coordinates(mesh); + +using ShaderAbstractions: data + +posmeta = Buffer(meta(data(pos_buff); color = data(vert_color_buff))) + +program = disp.renderlist[1][3].vertexarray.program + +struct OGLContext <: ShaderAbstractions.AbstractContext end + +instance = ShaderAbstractions.VertexArray(posmeta, triangles_buff) + +ShaderAbstractions.type_string(OGLContext(), Vec2f0) + +p = ShaderAbstractions.Program( + OGLContext(), + read(loadshader("mesh.vert"), String), + read(loadshader("mesh.frag"), String), + instance; +) + +println(p.vertex_source) + +uniforms = Dict{Symbol, Any}( + :texturecoordinates => Vec2f0(0), + :image => nothing +) +rshader = GLMakie.GLAbstraction.gl_convert(shader, uniforms) + +vbo = GLMakie.GLAbstraction.GLVertexArray(program, posmeta, triangles_buff) + +m = GeometryBasics.Mesh(posmeta, triangles_buff) +disp = display(Makie.mesh(m, show_axis=false)); + + +mesh_normals = GeometryBasics.normals(positions, triangles) +coords = meta(positions, color=xyz_vertex_color, normals=mesh_normals) +vertexcolor_mesh = GeometryBasics.Mesh(coords, triangles) +scren = mesh(vertexcolor_mesh, show_axis=false) |> display + + +function getter_function(io::IO, ::Fragment, sampler::Sampler, name::Symbol) + index_t = type_string(context, sampler.values) + sampler_t = type_string(context, sampler.colors) + + println(io, """ + in $(value_t) fragment_$(name)_index; + uniform $(sampler_t) $(name)_texture; + + vec4 get_$(name)(){ + return texture($(name)_texture, fragment_$(name)_index); + } + """) +end + +function getter_function(io::IO, ::Vertex, sampler::Sampler, name::Symbol) + index_t = type_string(context, sampler.values) + println(io, """ + in $(index_t) $(name)_index; + out $(index_t) fragment_$(name)_index; + + vec4 get_$(name)(){ + fragment_uv = uv; + // color gets calculated in fragment! + return vec4(0); + } + """) +end + +function getter_function(io::IO, ::Fragment, ::AbstractVector{T}, name) where T + t_str = type_string(context, T) + println(io, """ + in $(t_str) fragment_$(name); + $(t_str) get_$(name)(){ + return fragment_$(name); + } + """) +end + +function getter_function(io::IO, ::Vertex, ::AbstractVector{T}, name) where T + t_str = type_string(context, T) + println(io, """ + in $(t_str) $(name); + out $(t_str) fragment_$(name); + + $(t_str) get_$(name)(){ + fragment_$(name) = $(name); + return $(name); + } + """) +end + +texsampler = Makie.sampler(rand(RGBf0, 4, 4), uv) +coords = meta(positions, color=texsampler, normals=mesh_normals) +texture_mesh = GeometryBasics.Mesh(coords, triangles) + +scren = mesh(texture_mesh, show_axis=false) |> display + +texsampler = Makie.sampler(:viridis, rand(length(positions))) +coords = meta(positions, color=texsampler, normals=mesh_normals) +texture_mesh = GeometryBasics.Mesh(coords, triangles) + +scren = mesh(texture_mesh, show_axis=false) |> display + + + +glsl""" + +out vec2 frag_uv; +out vec3 frag_normal; +out vec3 frag_position; +out vec4 frag_color; +out vec3 frag_lightdir; + +uniform mat4 projectionMatrix; +uniform mat4 viewMatrix; +uniform mat4 modelMatrix; + +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;} + +void main(){ + // get_* gets the global inputs (uniform, sampler, vertex array) + // those functions will get inserted by the shader creation pipeline + vec3 vertex_position = tovec3(get_position()); + frag_normal = $(normals); + frag_uv = get_texturecoordinates(); + + vec3 lightpos = vec3(20); + vec4 position_world = modelMatrix * vec4(vertex_position, 1); + frag_lightdir = normalize(lightpos - position_world.xyz); + // direction to camera + frag_position = -position_world.xyz; + frag_uv = vec2(1.0 - frag_uv.y, frag_uv.x); + frag_color = tovec4(get_color()); + // screen space coordinates of the vertex + gl_Position = projectionMatrix * viewMatrix * position_world; +} +""" diff --git a/GLMakie/src/GLAbstraction/AbstractGPUArray.jl b/GLMakie/src/GLAbstraction/AbstractGPUArray.jl new file mode 100644 index 00000000000..d6f400949de --- /dev/null +++ b/GLMakie/src/GLAbstraction/AbstractGPUArray.jl @@ -0,0 +1,238 @@ +import Base: copy! +import Base: splice! +import Base: append! +import Base: push! +import Base: resize! +import Base: setindex! +import Base: getindex +import Base: map +import Base: length +import Base: eltype +import Base: lastindex +import Base: ndims +import Base: size +import Base: iterate +using Serialization + +abstract type GPUArray{T, NDim} <: AbstractArray{T, NDim} end + +length(A::GPUArray) = prod(size(A)) +eltype(b::GPUArray{T, NDim}) where {T, NDim} = T +lastindex(A::GPUArray) = length(A) +ndims(A::GPUArray{T, NDim}) where {T, NDim} = NDim +size(A::GPUArray) = A.size +size(A::GPUArray, i::Integer) = i <= ndims(A) ? A.size[i] : 1 + +function checkdimensions(value::Array, ranges::Union{Integer, UnitRange}...) + array_size = size(value) + indexes_size = map(length, ranges) + (array_size != indexes_size) && throw(DimensionMismatch("asigning a $array_size to a $(indexes_size) location")) + return true +end +function to_range(index) + map(index) do val + isa(val, Integer) && return val:val + isa(val, AbstractRange) && return val + error("Indexing only defined for integers or ranges. Found: $val") + end +end + +setindex!(A::GPUArray{T, N}, value::Union{T, Array{T, N}}) where {T, N} = (A[1] = value) + +function setindex!(A::GPUArray{T, N}, value, indices::Vararg{<: Integer, N}) where {T, N} + v = Array{T, N}(undef, ntuple(i-> 1, N)) + v[1] = convert(T, value) + setindex!(A, v, (:).(indices, indices)...) +end + +function setindex!(A::GPUArray{T, N}, value, indexes...) where {T, N} + ranges = to_range(Base.to_indices(A, indexes)) + v = isa(value, T) ? [value] : convert(Array{T,N}, value) + setindex!(A, v, ranges...) +end + +setindex!(A::GPUArray{T, 2}, value::Vector{T}, i::Integer, range::UnitRange) where {T} = + (A[i, range] = reshape(value, (length(value),1))) + +function setindex!(A::GPUArray{T, N}, value::Array{T, N}, ranges::UnitRange...) where {T, N} + checkbounds(A, ranges...) + checkdimensions(value, ranges...) + gpu_setindex!(A, value, ranges...) + nothing +end + +function update!(A::GPUArray{T, N}, value::AbstractArray{T2, N}) where {T, N, T2} + update!(A, convert(Array{T, N}, value)) +end +function update!(A::GPUArray{T, N}, value::AbstractArray{T, N}) where {T, N} + if length(A) != length(value) + if isa(A, GLBuffer) + resize!(A, length(value)) + elseif isa(A, Texture) + resize_nocopy!(A, size(value)) + elseif isa(A, TextureBuffer) + gpu_resize!(A, size(value)) + else + error("Dynamic resizing not implemented for $(typeof(A))") + end + end + dims = map(x-> 1:x, size(A)) + A[dims...] = value + nothing +end +update!(A::GPUArray, value::ShaderAbstractions.Sampler) = update!(A, value.data) + +function getindex(A::GPUArray{T, N}, i::Int) where {T, N} + checkbounds(A, i) + gpu_getindex(A, i:i)[1] # not as bad as its looks, as so far gpu data must be loaded into an array anyways +end +function getindex(A::GPUArray{T, N}, ranges::UnitRange...) where {T, N} + checkbounds(A, ranges...) + gpu_getindex(A, ranges...) +end + +mutable struct GPUVector{T} <: GPUArray{T, 1} + buffer + size + real_length +end + +GPUVector(x::GPUArray) = GPUVector{eltype(x)}(x, size(x), length(x)) + +function update!(A::GPUVector{T}, value::AbstractVector{T}) where T + if isa(A, GLBuffer) && (length(A) != length(value)) + resize!(A, length(value)) + end + dims = map(x->1:x, size(A)) + A.buffer[dims...] = value + return nothing +end + +length(v::GPUVector) = prod(size(v)) +size(v::GPUVector) = v.size +size(v::GPUVector, i::Integer) = v.size[i] +ndims(::GPUVector) = 1 +eltype(::GPUVector{T}) where {T} = T +lastindex(A::GPUVector) = length(A) + + +iterate(b::GPUVector, state = 1) = iterate(b.buffer, state) + +gpu_data(A::GPUVector) = A.buffer[1:length(A)] + +getindex(v::GPUVector, index::Int) = v.buffer[index] +getindex(v::GPUVector, index::UnitRange) = v.buffer[index] +setindex!(v::GPUVector{T}, value::T, index::Int) where {T} = v.buffer[index] = value +setindex!(v::GPUVector{T}, value::T, index::UnitRange) where {T} = v.buffer[index] = value + + +function grow_dimensions(real_length::Int, _size::Int, additonal_size::Int, growfactor::Real=1.5) + new_dim = round(Int, real_length*growfactor) + return max(new_dim, additonal_size+_size) +end +function Base.push!(v::GPUVector{T}, x::AbstractVector{T}) where T + lv, lx = length(v), length(x) + if (v.real_length < lv+lx) + resize!(v.buffer, grow_dimensions(v.real_length, lv, lx)) + end + v.buffer[lv+1:(lv+lx)] = x + v.real_length = length(v.buffer) + v.size = (lv+lx,) + v +end +push!(v::GPUVector{T}, x::T) where {T} = push!(v, [x]) +push!(v::GPUVector{T}, x::T...) where {T} = push!(v, [x...]) +append!(v::GPUVector{T}, x::Vector{T}) where {T} = push!(v, x) + +resize!(A::GPUArray{T, NDim}, dims::Int...) where {T, NDim} = resize!(A, dims) +function resize!(A::GPUArray{T, NDim}, newdims::NTuple{NDim, Int}) where {T, NDim} + newdims == size(A) && return A + gpu_resize!(A, newdims) + A +end + +function resize!(v::GPUVector, newlength::Int) + if v.real_length >= newlength # is still big enough + v.size = (max(0, newlength),) + return v + end + resize!(v.buffer, grow_dimensions(v.real_length, length(v), newlength-length(v))) + v.size = (newlength,) + v.real_length = length(v.buffer) +end +function grow_at(v::GPUVector, index::Int, amount::Int) + resize!(v, length(v)+amount) + copy!(v, index, v, index+amount, amount) +end + +function splice!(v::GPUVector{T}, index::UnitRange, x::Vector=T[]) where T + lenv = length(v) + elements_to_grow = length(x)-length(index) # -1 + buffer = similar(v.buffer, length(v)+elements_to_grow) + copy!(v.buffer, 1, buffer, 1, first(index)-1) # copy first half + copy!(v.buffer, last(index)+1, buffer, first(index)+length(x), lenv-last(index)) # shift second half + v.buffer = buffer + v.real_length = length(buffer) + v.size = (v.real_length,) + copy!(x, 1, buffer, first(index), length(x)) # copy contents of insertion vector + nothing +end +splice!(v::GPUVector{T}, index::Int, x::T) where {T} = v[index] = x +splice!(v::GPUVector{T}, index::Int, x::Vector=T[]) where {T} = splice!(v, index:index, map(T, x)) + + +copy!(a::GPUVector, a_offset::Int, b::Vector, b_offset::Int, amount::Int) = copy!(a.buffer, a_offset, b, b_offset, amount) +copy!(a::GPUVector, a_offset::Int, b::GPUVector, b_offset::Int, amount::Int)= copy!(a.buffer, a_offset, b.buffer, b_offset, amount) + + +copy!(a::GPUArray, a_offset::Int, b::Vector, b_offset::Int, amount::Int) = _copy!(a, a_offset, b, b_offset, amount) +copy!(a::Vector, a_offset::Int, b::GPUArray, b_offset::Int, amount::Int) = _copy!(a, a_offset, b, b_offset, amount) +copy!(a::GPUArray, a_offset::Int, b::GPUArray, b_offset::Int, amount::Int) = _copy!(a, a_offset, b, b_offset, amount) + +#don't overwrite Base.copy! with a::Vector, b::Vector +function _copy!(a::Union{Vector, GPUArray}, a_offset::Int, b::Union{Vector, GPUArray}, b_offset::Int, amount::Int) + (amount <= 0) && return nothing + @assert a_offset > 0 && (a_offset-1) + amount <= length(a) "a_offset $a_offset, amount $amount, lengtha $(length(a))" + @assert b_offset > 0 && (b_offset-1) + amount <= length(b) "b_offset $b_offset, amount $amount, lengthb $(length(b))" + unsafe_copy!(a, a_offset, b, b_offset, amount) + return nothing +end + +# Interface: +gpu_data(t) = error("gpu_data not implemented for: $(typeof(t)). This happens, when you call data on an array, without implementing the GPUArray interface") +gpu_resize!(t) = error("gpu_resize! not implemented for: $(typeof(t)). This happens, when you call resize! on an array, without implementing the GPUArray interface") +gpu_getindex(t) = error("gpu_getindex not implemented for: $(typeof(t)). This happens, when you call getindex on an array, without implementing the GPUArray interface") +gpu_setindex!(t) = error("gpu_setindex! not implemented for: $(typeof(t)). This happens, when you call setindex! on an array, without implementing the GPUArray interface") +max_dim(t) = error("max_dim not implemented for: $(typeof(t)). This happens, when you call setindex! on an array, without implementing the GPUArray interface") + + +function (::Type{T})(x::Node; kw...) where T <: GPUArray + gpu_mem = T(x[]; kw...) + on(x-> update!(gpu_mem, x), x) + gpu_mem +end + +const BaseSerializer = Serialization.AbstractSerializer + +function Serialization.serialize(s::BaseSerializer, t::T) where T<:GPUArray + Serialization.serialize_type(s, T) + Serialization.serialize(s, Array(t)) +end +function Serialization.deserialize(s::BaseSerializer, ::Type{T}) where T <: GPUArray + A = Serialization.deserialize(s) + T(A) +end + + +export data +export resize +export GPUArray +export GPUVector + +export update! + +export gpu_data +export gpu_resize! +export gpu_getindex +export gpu_setindex! +export max_dim diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl new file mode 100644 index 00000000000..977235ae5c5 --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -0,0 +1,115 @@ +module GLAbstraction + +using StaticArrays +using GeometryBasics +using ModernGL +using Makie +using FixedPointNumbers +using ColorTypes +using ..GLMakie.GLFW +using Printf +using LinearAlgebra +using Observables +using ShaderAbstractions +using ShaderAbstractions: current_context, is_context_active, context_alive + +import FixedPointNumbers: N0f8, N0f16, N0f8, Normed + +import Makie: update! + +import Base: merge, resize!, similar, length, getindex, setindex! + +include("AbstractGPUArray.jl") + +#Methods which get overloaded by GLExtendedFunctions.jl: +import ModernGL.glShaderSource +import ModernGL.glGetAttachedShaders +import ModernGL.glGetActiveUniform +import ModernGL.glGetActiveAttrib +import ModernGL.glGetProgramiv +import ModernGL.glGetIntegerv +import ModernGL.glGenBuffers +import ModernGL.glGetProgramiv +import ModernGL.glGenVertexArrays +import ModernGL.glGenTextures +import ModernGL.glGenFramebuffers +import ModernGL.glGetTexLevelParameteriv +import ModernGL.glGenRenderbuffers +import ModernGL.glDeleteTextures +import ModernGL.glDeleteVertexArrays +import ModernGL.glDeleteBuffers +import ModernGL.glGetShaderiv +import ModernGL.glViewport +import ModernGL.glScissor + +include("GLUtils.jl") +export @materialize #splats keywords from a dict into variables +export @materialize! #splats keywords from a dict into variables and deletes them from the dict +export close_to_square +export AND, OR, isnotempty + +include("shaderabstraction.jl") +include("GLTypes.jl") +export GLProgram # Shader/program object +export Texture # Texture object, basically a 1/2/3D OpenGL data array +export TextureParameters +export TextureBuffer # OpenGL texture buffer +export update! # updates a gpu array with a Julia array +export gpu_data # gets the data of a gpu array as a Julia Array + +export RenderObject # An object which holds all GPU handles and datastructes to ready for rendering by calling render(obj) +export prerender! # adds a function to a RenderObject, which gets executed befor setting the OpenGL render state +export postrender! # adds a function to a RenderObject, which gets executed after setting the OpenGL render states +export std_renderobject # creates a renderobject with standard parameters +export instanced_renderobject # simplification for creating a RenderObject which renders instances +export extract_renderable +export set_arg! +export GLVertexArray # VertexArray wrapper object +export GLBuffer # OpenGL Buffer object wrapper +export indexbuffer # Shortcut to create an OpenGL Buffer object for indexes (1D, cardinality of one and GL_ELEMENT_ARRAY_BUFFER set) +export opengl_compatible # infers if a type is opengl compatible and returns stats like cardinality and eltype (will be deprecated) +export cardinality # returns the cardinality of the elements of a buffer + +export Style # Style Type, which is used to choose different visualization/editing styles via multiple dispatch +export mergedefault! # merges a style dict via a given style +export TOrSignal, VecOrSignal, ArrayOrSignal, MatOrSignal, VolumeOrSignal, ArrayTypes, VectorTypes, MatTypes, VolumeTypes +export MouseButton, MOUSE_LEFT, MOUSE_MIDDLE, MOUSE_RIGHT + +include("GLExtendedFunctions.jl") +export glTexImage # Julian wrapper for glTexImage1D, glTexImage2D, glTexImage3D +include("GLShader.jl") +export Shader #Shader Type +export readshader #reads a shader +export glsl_variable_access # creates access string from julia variable for the use in glsl shaders +export createview #creates a view from a templated shader +export TemplateProgram # Creates a shader from a Mustache view and and a shader file, which uses mustache syntax to replace values. +export @comp_str #string macro for the different shader types. +export @frag_str # with them you can write frag""" ..... """, returning shader object +export @vert_str +export @geom_str +export AbstractLazyShader, LazyShader + +include("GLUniforms.jl") +export gluniform # wrapper of all the OpenGL gluniform functions, which call the correct gluniform function via multiple dispatch. Example: gluniform(location, x::Matrix4x4) = gluniformMatrix4fv(location, x) +export toglsltype_string # infers a glsl type string from a julia type. Example: Matrix4x4 -> uniform mat4 +# Also exports Macro generated GLSL alike aliases for Float32 Matrices and Vectors +# only difference to GLSL: first character is uppercase uppercase +export gl_convert + + +include("GLRender.jl") +export render #renders arbitrary objects +export enabletransparency # can be pushed to an renderobject, enables transparency +export renderinstanced # renders objects instanced + +include("GLInfo.jl") +export getUniformsInfo +export getProgramInfo +export getAttributesInfo + +if Base.VERSION >= v"1.4.2" + include("precompile.jl") + _precompile_() +end + +end # module diff --git a/GLMakie/src/GLAbstraction/GLBuffer.jl b/GLMakie/src/GLAbstraction/GLBuffer.jl new file mode 100644 index 00000000000..8b0bcb91133 --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLBuffer.jl @@ -0,0 +1,199 @@ +mutable struct GLBuffer{T} <: GPUArray{T, 1} + id ::GLuint + size ::NTuple{1, Int} + buffertype ::GLenum + usage ::GLenum + context ::GLContext + + function GLBuffer{T}(ptr::Ptr{T}, buff_length::Int, buffertype::GLenum, usage::GLenum) where T + id = glGenBuffers() + glBindBuffer(buffertype, id) + # size of 0 can segfault it seems + buff_length = buff_length == 0 ? 1 : buff_length + glBufferData(buffertype, buff_length * sizeof(T), ptr, usage) + glBindBuffer(buffertype, 0) + + obj = new(id, (buff_length,), buffertype, usage, current_context()) + finalizer(free, obj) + obj + end +end + +bind(buffer::GLBuffer) = glBindBuffer(buffer.buffertype, buffer.id) +#used to reset buffer target +bind(buffer::GLBuffer, other_target) = glBindBuffer(buffer.buffertype, other_target) + +function similar(x::GLBuffer{T}, buff_length::Int) where T + GLBuffer{T}(Ptr{T}(C_NULL), buff_length, x.buffertype, x.usage) +end + +cardinality(::GLBuffer{T}) where {T} = cardinality(T) + +#Function to deal with any Immutable type with Real as Subtype +function GLBuffer( + buffer::Union{Base.ReinterpretArray{T, 1}, DenseVector{T}}; + buffertype::GLenum = GL_ARRAY_BUFFER, usage::GLenum = GL_STATIC_DRAW + ) where T <: GLArrayEltypes + GLBuffer{T}(pointer(buffer), length(buffer), buffertype, usage) +end + +function GLBuffer( + buffer::DenseVector{T}; + buffertype::GLenum = GL_ARRAY_BUFFER, usage::GLenum = GL_STATIC_DRAW + ) where T <: GLArrayEltypes + GLBuffer{T}(pointer(buffer), length(buffer), buffertype, usage) +end + +function GLBuffer( + buffer::AbstractVector{T}; + kw_args... + ) where T <: GLArrayEltypes + GLBuffer(collect(buffer); kw_args...) +end + +function GLBuffer{T}( + buffer::AbstractVector; + kw_args... + ) where T <: GLArrayEltypes + GLBuffer(convert(Vector{T}, buffer); kw_args...) +end + +function GLBuffer( + ::Type{T}, len::Int; + buffertype::GLenum = GL_ARRAY_BUFFER, usage::GLenum = GL_STATIC_DRAW + ) where T <: GLArrayEltypes + GLBuffer{T}(Ptr{T}(C_NULL), len, buffertype, usage) +end + + +function indexbuffer( + buffer::VectorTypes{T}; + usage::GLenum = GL_STATIC_DRAW + ) where T <: GLArrayEltypes + GLBuffer(buffer, buffertype = GL_ELEMENT_ARRAY_BUFFER, usage=usage) +end +# GPUArray interface +function gpu_data(b::GLBuffer{T}) where T + data = Vector{T}(undef, length(b)) + bind(b) + glGetBufferSubData(b.buffertype, 0, sizeof(data), data) + bind(b, 0) + data +end + + +# Resize buffer +function gpu_resize!(buffer::GLBuffer{T}, newdims::NTuple{1, Int}) where T + #TODO make this safe! + newlength = newdims[1] + oldlen = length(buffer) + if oldlen > 0 + old_data = gpu_data(buffer) + end + bind(buffer) + glBufferData(buffer.buffertype, newlength*sizeof(T), C_NULL, buffer.usage) + bind(buffer, 0) + buffer.size = newdims + if oldlen>0 + max_len = min(length(old_data), newlength) #might also shrink + buffer[1:max_len] = old_data[1:max_len] + end + #probably faster, but changes the buffer ID + # newbuff = similar(buffer, newdims...) + # unsafe_copy!(buffer, 1, newbuff, 1, length(buffer)) + # buffer.id = newbuff.id + # buffer.size = newbuff.size + nothing +end + +function gpu_setindex!(b::GLBuffer{T}, value::Vector{T}, offset::Integer) where T + multiplicator = sizeof(T) + bind(b) + glBufferSubData(b.buffertype, multiplicator*(offset-1), sizeof(value), value) + bind(b, 0) +end + +function gpu_setindex!(b::GLBuffer{T}, value::Vector{T}, offset::UnitRange{Int}) where T + multiplicator = sizeof(T) + bind(b) + glBufferSubData(b.buffertype, multiplicator*(first(offset)-1), sizeof(value), value) + bind(b, 0) + return nothing +end + +# copy between two buffers +# could be a setindex! operation, with subarrays for buffers +function unsafe_copy!(a::GLBuffer{T}, readoffset::Int, b::GLBuffer{T}, writeoffset::Int, len::Int) where T + multiplicator = sizeof(T) + glBindBuffer(GL_COPY_READ_BUFFER, a.id) + glBindBuffer(GL_COPY_WRITE_BUFFER, b.id) + glCopyBufferSubData( + GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, + multiplicator*(readoffset-1), + multiplicator*(writeoffset-1), + multiplicator*len + ) + glBindBuffer(GL_COPY_READ_BUFFER, 0) + glBindBuffer(GL_COPY_WRITE_BUFFER, 0) + return nothing +end + +function Base.iterate(buffer::GLBuffer{T}) where T + length(buffer) < 1 && return nothing + glBindBuffer(buffer.buffertype, buffer.id) + ptr = Ptr{T}(glMapBuffer(buffer.buffertype, GL_READ_WRITE)) + return (unsafe_load(ptr, i), (ptr, 1)) +end + +function Base.iterate(buffer::GLBuffer{T}, state::Tuple{Ptr{T}, Int}) where T + ptr, i = state + if i > length(buffer) + glUnmapBuffer(buffer.buffertype) + return nothing + end + val = unsafe_load(ptr, i) + return (val, (ptr, i + 1)) +end + +#copy inside one buffer +function unsafe_copy!(buffer::GLBuffer{T}, readoffset::Int, writeoffset::Int, len::Int) where T + len <= 0 && return nothing + bind(buffer) + ptr = Ptr{T}(glMapBuffer(buffer.buffertype, GL_READ_WRITE)) + for i=1:len+1 + unsafe_store!(ptr, unsafe_load(ptr, i+readoffset-1), i+writeoffset-1) + end + glUnmapBuffer(buffer.buffertype) + bind(buffer,0) + return nothing +end + +function unsafe_copy!(a::Vector{T}, readoffset::Int, b::GLBuffer{T}, writeoffset::Int, len::Int) where T + bind(b) + ptr = Ptr{T}(glMapBuffer(b.buffertype, GL_WRITE_ONLY)) + for i=1:len + unsafe_store!(ptr, a[i+readoffset-1], i+writeoffset-1) + end + glUnmapBuffer(b.buffertype) + bind(b,0) +end + +function unsafe_copy!(a::GLBuffer{T}, readoffset::Int, b::Vector{T}, writeoffset::Int, len::Int) where T + bind(a) + ptr = Ptr{T}(glMapBuffer(a.buffertype, GL_READ_ONLY)) + for i in 1:len + b[i+writeoffset-1] = unsafe_load(ptr, i+readoffset-2) #-2 => -1 to zero offset, -1 gl indexing starts at 0 + end + glUnmapBuffer(a.buffertype) + bind(a,0) +end + +function gpu_getindex(b::GLBuffer{T}, range::UnitRange) where T + multiplicator = sizeof(T) + offset = first(range)-1 + value = Vector{T}(undef, length(range)) + bind(b) + glGetBufferSubData(b.buffertype, multiplicator*offset, sizeof(value), value) + bind(b, 0) + return value +end diff --git a/GLMakie/src/GLAbstraction/GLExtendedFunctions.jl b/GLMakie/src/GLAbstraction/GLExtendedFunctions.jl new file mode 100644 index 00000000000..80b13b64804 --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLExtendedFunctions.jl @@ -0,0 +1,261 @@ +#= +This is the place, where I put functions, which are so annoying in OpenGL, that I felt the need to wrap them and make them more "Julian" +Its also to do some more complex error handling, not handled by the debug callback +=# + +function glGetShaderiv(shaderID::GLuint, variable::GLenum) + result = Ref{GLint}(-1) + glGetShaderiv(shaderID, variable, result) + result[] +end +function glShaderSource(shaderID::GLuint, shadercode::Vector{UInt8}) + shader_code_ptrs = Ptr{UInt8}[pointer(shadercode)] + len = Ref{GLint}(length(shadercode)) + glShaderSource(shaderID, 1, shader_code_ptrs, len) +end +glShaderSource(shaderID::GLuint, shadercode::String) = glShaderSource(shaderID, Vector{UInt8}(shadercode)) +function glGetAttachedShaders(program::GLuint) + shader_count = glGetProgramiv(program, GL_ATTACHED_SHADERS) + length_written = GLsizei[0] + shaders = zeros(GLuint, shader_count) + + glGetAttachedShaders(program, shader_count, length_written, shaders) + shaders[1:first(length_written)] +end + +get_attribute_location(program::GLuint, name) = get_attribute_location(program, ascii(name)) +get_attribute_location(program::GLuint, name::Symbol) = get_attribute_location(program, string(name)) +function get_attribute_location(program::GLuint, name::String) + location::GLint = glGetAttribLocation(program, name) + if location == -1 + # warn( + # "Named attribute (:$(name)) is not an active attribute in the specified program object or\n + # the name starts with the reserved prefix gl_\n" + # ) + elseif location == GL_INVALID_OPERATION + error( + "program is not a value generated by OpenGL or\n + program is not a program object or\n + program has not been successfully linked" + ) + end + location +end + + +get_uniform_location(program::GLuint, name::Symbol) = get_uniform_location(program, String(name)) +function get_uniform_location(program::GLuint, name::String) + location = glGetUniformLocation(program, name) + if location == -1 + error( + """Named uniform (:$(name)) is not an active attribute in the specified program object or + the name starts with the reserved prefix gl_""" + ) + elseif location == GL_INVALID_OPERATION + error("""program is not a value generated by OpenGL or + program is not a program object or + program has not been successfully linked""" + ) + end + location +end + +function glGetActiveUniform(programID::GLuint, index::Integer) + actualLength = GLsizei[1] + uniformSize = GLint[1] + typ = GLenum[1] + maxcharsize = glGetProgramiv(programID, GL_ACTIVE_UNIFORM_MAX_LENGTH) + name = Vector{GLchar}(undef, maxcharsize) + + glGetActiveUniform(programID, index, maxcharsize, actualLength, uniformSize, typ, name) + + actualLength[1] <= 0 && error("No active uniform at given index. Index: ", index) + + uname = unsafe_string(pointer(name), actualLength[1]) + uname = Symbol(replace(uname, r"\[\d*\]" => "")) # replace array brackets. This is not really a good solution. + return (uname, typ[1], uniformSize[1]) +end + +function glGetActiveAttrib(programID::GLuint, index::Integer) + actualLength = GLsizei[1] + attributeSize = GLint[1] + typ = GLenum[1] + maxcharsize = glGetProgramiv(programID, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH) + name = Vector{GLchar}(undef, maxcharsize) + + glGetActiveAttrib(programID, index, maxcharsize, actualLength, attributeSize, typ, name) + + actualLength[1] <= 0 && error("No active uniform at given index. Index: ", index) + + uname = unsafe_string(pointer(name), actualLength[1]) + uname = Symbol(replace(uname, r"\[\d*\]" => "")) # replace array brackets. This is not really a good solution. + return (uname, typ[1], attributeSize[1]) +end + +function glGetProgramiv(programID::GLuint, variable::GLenum) + result = Ref{GLint}(-1) + glGetProgramiv(programID, variable, result) + return result[] +end + +function glGetIntegerv(variable::GLenum) + result = Ref{GLint}(-1) + glGetIntegerv(UInt32(variable), result) + return result[] +end + +function glGenBuffers(n=1) + result = GLuint[0] + glGenBuffers(1, result) + id = result[] + if id <= 0 + error("glGenBuffers returned invalid id. OpenGL Context active?") + end + return id +end + +function glGenVertexArrays() + result = GLuint[0] + glGenVertexArrays(1, result) + id = result[1] + if id <=0 + error("glGenVertexArrays returned invalid id. OpenGL Context active?") + end + return id +end + +function glGenTextures() + result = GLuint[0] + glGenTextures(1, result) + id = result[1] + if id <= 0 + error("glGenTextures returned invalid id. OpenGL Context active?") + end + return id +end + +function glGenFramebuffers() + result = GLuint[0] + glGenFramebuffers(1, result) + id = result[1] + if id <= 0 + error("glGenFramebuffers returned invalid id. OpenGL Context active?") + end + return id +end + +function glDeleteTextures(id::GLuint) + arr = [id] + glDeleteTextures(1, arr) +end + +function glDeleteVertexArrays(id::GLuint) + arr = [id] + glDeleteVertexArrays(1, arr) +end + +function glDeleteBuffers(id::GLuint) + arr = [id] + glDeleteBuffers(1, arr) +end + +function glGetTexLevelParameteriv(target::GLenum, level, name::GLenum) + result = GLint[0] + glGetTexLevelParameteriv(target, level, name, result) + result[1] +end + +glViewport(x::Rect2D) = glViewport(minimum(x)..., widths(x)...) +glScissor(x::Rect2D) = glScissor(minimum(x)..., widths(x)...) + +function glGenRenderbuffers(format::GLenum, attachment::GLenum, dimensions) + renderbuffer = GLuint[0] + glGenRenderbuffers(1, renderbuffer) + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer[1]) + glRenderbufferStorage(GL_RENDERBUFFER, format, dimensions...) + glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, renderbuffer[1]) + return renderbuffer[1] +end + +function glTexImage(ttype::GLenum, level::Integer, internalFormat::GLenum, w::Integer, h::Integer, d::Integer, border::Integer, format::GLenum, datatype::GLenum, data) + glTexImage3D(GL_PROXY_TEXTURE_3D, level, internalFormat, w, h, d, border, format, datatype, C_NULL) + for l in 0:level + result = glGetTexLevelParameteriv(GL_PROXY_TEXTURE_3D, l, GL_TEXTURE_WIDTH) + if result == 0 + error("glTexImage 3D: width too large. Width: ", w) + end + result = glGetTexLevelParameteriv(GL_PROXY_TEXTURE_3D, l,GL_TEXTURE_HEIGHT) + if result == 0 + error("glTexImage 3D: height too large. height: ", h) + end + result = glGetTexLevelParameteriv(GL_PROXY_TEXTURE_3D, l, GL_TEXTURE_DEPTH) + if result == 0 + error("glTexImage 3D: depth too large. Depth: ", d) + end + result = glGetTexLevelParameteriv(GL_PROXY_TEXTURE_3D, l, GL_TEXTURE_INTERNAL_FORMAT) + if result == 0 + error("glTexImage 3D: internal format not valid. format: ", GLENUM(internalFormat).name) + end + end + glTexImage3D(ttype, level, internalFormat, w, h, d, border, format, datatype, data) +end + +function glTexImage(ttype::GLenum, level::Integer, internalFormat::GLenum, w::Integer, h::Integer, border::Integer, format::GLenum, datatype::GLenum, data) + maxsize = glGetIntegerv(GL_MAX_TEXTURE_SIZE) + glTexImage2D(GL_PROXY_TEXTURE_2D, level, internalFormat, w, h, border, format, datatype, C_NULL) + for l in 0:level + result = glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, l, GL_TEXTURE_WIDTH) + if result == 0 + error("glTexImage 2D: width too large. Width: ", w) + end + result = glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, l, GL_TEXTURE_HEIGHT) + if result == 0 + error("glTexImage 2D: height too large. height: ", h) + end + result = glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, l, GL_TEXTURE_INTERNAL_FORMAT) + if result == 0 + error("glTexImage 2D: internal format not valid. format: ", GLENUM(internalFormat).name) + end + end + glTexImage2D(ttype, level, internalFormat, w, h, border, format, datatype, data) +end + +function glTexImage(ttype::GLenum, level::Integer, internalFormat::GLenum, w::Integer, border::Integer, format::GLenum, datatype::GLenum, data) + glTexImage1D(GL_PROXY_TEXTURE_1D, level, internalFormat, w, border, format, datatype, C_NULL) + for l in 0:level + result = glGetTexLevelParameteriv(GL_PROXY_TEXTURE_1D, l, GL_TEXTURE_WIDTH) + if result == 0 + error("glTexImage 1D: width too large. Width: ", w) + end + result = glGetTexLevelParameteriv(GL_PROXY_TEXTURE_1D, l, GL_TEXTURE_INTERNAL_FORMAT) + if result == 0 + error("glTexImage 1D: internal format not valid. format: ", GLENUM(internalFormat).name) + end + end + glTexImage1D(ttype, level, internalFormat, w, border, format, datatype, data) +end + +function glsl_version_number() + glsl = split(unsafe_string(glGetString(GL_SHADING_LANGUAGE_VERSION)), ['.', ' ']) + if length(glsl) >= 2 + return VersionNumber(parse(Int, glsl[1]), parse(Int, glsl[2])) + else + error("could not parse GLSL version: $glsl") + end +end + +function opengl_version_number() + ogl = split(unsafe_string(glGetString(GL_VERSION)), ['.', ' ']) + if length(ogl) >= 3 + return VersionNumber(parse(Int, ogl[1]), parse(Int, ogl[2]), parse(Int, ogl[3])) + else + error("could not parse OpenGL version: $ogl") + end +end + +function glsl_version_string() + glsl = glsl_version_number() + glsl.major == 1 && glsl.minor <= 2 && error("OpenGL shading Language version too low. Try updating graphic driver!") + glsl_version = string(glsl.major) * rpad(string(glsl.minor), 2, "0") + return "#version $(glsl_version)\n" +end diff --git a/GLMakie/src/GLAbstraction/GLInfo.jl b/GLMakie/src/GLAbstraction/GLInfo.jl new file mode 100644 index 00000000000..c72c677807f --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLInfo.jl @@ -0,0 +1,99 @@ +getnames(check_function::Function) = filter(check_function, uint32(0:65534)) + +# gets all the names currently boundo to programs +getProgramNames() = getnames(glIsProgram) +getShaderNames() = getnames(glIsShader) +getVertexArrayNames() = getnames(glIsVertexArray) + +# display info for all active uniforms in a program +function getUniformsInfo(p::GLProgram) + program = p.id + # Get uniforms info (not in named blocks) + @show activeUnif = glGetProgramiv(program, GL_ACTIVE_UNIFORMS) + bufSize = 16 + name = zeros(UInt8, bufSize) + buflen = Ref{GLsizei}(0) + size = Ref{GLint}(0) + type = Ref{GLenum}() + + for i=0:activeUnif-1 + glGetActiveUniform(program, i, bufSize, buflen, size, type, name) + println(String(name), " ", buflen[], " ", size[], " ", GLENUM(type[]).name) + end +end + +function uniform_name_type(p::GLProgram, location) + bufSize = 32 + name = zeros(UInt8, bufSize) + buflen = Ref{GLsizei}(0) + size = Ref{GLint}(0) + type = Ref{GLenum}() + glGetActiveUniform(p.id, location, bufSize, buflen, size, type, name) + println(String(name), " ", buflen[], " ", size[], " ", GLENUM(type[]).name) +end + +# display the values for uniforms in the default block +function getUniformInfo(p::GLProgram, uniName::Symbol) + # is it a program ? + @show program = p.id + @show loc = glGetUniformLocation(program, uniName) + @show name, typ, uniform_size = glGetActiveUniform(program, loc) +end + + +# display the values for a uniform in a named block +function getUniformInBlockInfo(p::GLProgram, blockName, uniName) + program = p.id + + @show index = glGetUniformBlockIndex(program, blockName) + if (index == GL_INVALID_INDEX) + println("$uniName is not a valid uniform name in block $blockName") + end + @show bindIndex = glGetActiveUniformBlockiv(program, index, GL_UNIFORM_BLOCK_BINDING) + @show bufferIndex = glGetIntegeri_v(GL_UNIFORM_BUFFER_BINDING, bindIndex) + @show uniIndex = glGetUniformIndices(program, uniName) + + @show uniType = glGetActiveUniformsiv(program, uniIndex, GL_UNIFORM_TYPE) + @show uniOffset = glGetActiveUniformsiv(program, uniIndex, GL_UNIFORM_OFFSET) + @show uniSize = glGetActiveUniformsiv(program, uniIndex, GL_UNIFORM_SIZE) + @show uniArrayStride = glGetActiveUniformsiv(program, uniIndex, GL_UNIFORM_ARRAY_STRIDE) + @show uniMatStride = glGetActiveUniformsiv(program, uniIndex, GL_UNIFORM_MATRIX_STRIDE) +end + + +# display information for a program's attributes +function getAttributesInfo(p::GLProgram) + + program = p.id + # how many attribs? + @show activeAttr = glGetProgramiv(program, GL_ACTIVE_ATTRIBUTES) + # get location and type for each attrib + for i=0:activeAttr-1 + @show name, typ, siz = glGetActiveAttrib(program, i) + @show loc = glGetAttribLocation(program, name) + end +end + + +# display program's information +function getProgramInfo(p::GLProgram) + # check if name is really a program + @show program = p.id + # Get the shader's name + @show shaders = glGetAttachedShaders(program) + for shader in shaders + @show info = GLENUM(convert(GLenum, glGetShaderiv(shader, GL_SHADER_TYPE))).name + end + # Get program info + @show info = glGetProgramiv(program, GL_PROGRAM_SEPARABLE) + @show info = glGetProgramiv(program, GL_PROGRAM_BINARY_RETRIEVABLE_HINT) + @show info = glGetProgramiv(program, GL_LINK_STATUS) + @show info = glGetProgramiv(program, GL_VALIDATE_STATUS) + @show info = glGetProgramiv(program, GL_DELETE_STATUS) + @show info = glGetProgramiv(program, GL_ACTIVE_ATTRIBUTES) + @show info = glGetProgramiv(program, GL_ACTIVE_UNIFORMS) + @show info = glGetProgramiv(program, GL_ACTIVE_UNIFORM_BLOCKS) + @show info = glGetProgramiv(program, GL_ACTIVE_ATOMIC_COUNTER_BUFFERS) + @show info = glGetProgramiv(program, GL_TRANSFORM_FEEDBACK_BUFFER_MODE) + @show info = glGetProgramiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS) +end diff --git a/GLMakie/src/GLAbstraction/GLRender.jl b/GLMakie/src/GLAbstraction/GLRender.jl new file mode 100644 index 00000000000..0c4bb247a47 --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLRender.jl @@ -0,0 +1,161 @@ +function render(list::Tuple) + for elem in list + render(elem) + end + return +end +""" +When rendering a specialised list of Renderables, we can do some optimizations +""" +function render(list::Vector{RenderObject{Pre}}) where Pre + isempty(list) && return nothing + first(list).prerenderfunction() + vertexarray = first(list).vertexarray + program = vertexarray.program + glUseProgram(program.id) + glBindVertexArray(vertexarray.id) + for renderobject in list + Bool(to_value(renderobject.uniforms[:visible])) || continue # skip invisible + # make sure we only bind new programs and vertexarray when it is actually + # different from the previous one + if renderobject.vertexarray != vertexarray + vertexarray = renderobject.vertexarray + if vertexarray.program != program + program = renderobject.vertexarray.program + glUseProgram(program.id) + end + glBindVertexArray(vertexarray.id) + end + for (key, value) in program.uniformloc + if haskey(renderobject.uniforms, key) + if length(value) == 1 + gluniform(value[1], renderobject.uniforms[key]) + elseif length(value) == 2 + gluniform(value[1], value[2], renderobject.uniforms[key]) + else + error("Uniform tuple too long: $(length(value))") + end + end + end + renderobject.postrenderfunction() + end + # we need to assume, that we're done here, which is why + # we need to bind VertexArray to 0. + # Otherwise, every glBind(::GLBuffer) operation will be recorded into the state + # of the currently bound vertexarray + glBindVertexArray(0) + return +end + +""" +Renders a RenderObject +Note, that this function is not optimized at all! +It uses dictionaries and doesn't care about OpenGL call optimizations. +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) + if Bool(to_value(renderobject.uniforms[:visible])) + renderobject.prerenderfunction() + program = vertexarray.program + glUseProgram(program.id) + for (key, value) in program.uniformloc + if haskey(renderobject.uniforms, key) + # uniform_name_type(program, value[1]) + if length(value) == 1 + gluniform(value[1], renderobject.uniforms[key]) + elseif length(value) == 2 + gluniform(value[1], value[2], renderobject.uniforms[key]) + else + error("Uniform tuple too long: $(length(value))") + end + end + end + glBindVertexArray(vertexarray.id) + renderobject.postrenderfunction() + glBindVertexArray(0) + end + return +end + + +""" +Renders a vertexarray, which consists of the usual buffers plus a vector of +unitranges which defines the segments of the buffers to be rendered +""" +function render(vao::GLVertexArray{T}, mode::GLenum=GL_TRIANGLES) where T <: VecOrSignal{UnitRange{Int}} + for elem in to_value(vao.indices) + glDrawArrays(mode, max(first(elem) - 1, 0), length(elem) + 1) + end + return nothing +end + +function render(vao::GLVertexArray{T}, mode::GLenum=GL_TRIANGLES) where T <: TOrSignal{UnitRange{Int}} + r = to_value(vao.indices) + offset = first(r) - 1 # 1 based -> 0 based + ndraw = length(r) + nverts = length(vao) + if (offset < 0 || offset + ndraw > nverts) + error("Bounds error for drawrange. Offset $(offset) and length $(ndraw) aren't a valid range for vertexarray with length $(nverts)") + end + glDrawArrays(mode, offset, ndraw) + return nothing +end + +function render(vao::GLVertexArray{T}, mode::GLenum=GL_TRIANGLES) where T <: TOrSignal{Int} + r = to_value(vao.indices) + glDrawArrays(mode, 0, r) + return nothing +end + +""" +Renders a vertex array which supplies an indexbuffer +""" +function render(vao::GLVertexArray{GLBuffer{T}}, mode::GLenum=GL_TRIANGLES) where T <: Union{Integer,AbstractFace} + glDrawElements( + mode, + length(vao.indices) * cardinality(vao.indices), + julia2glenum(T), C_NULL + ) + return +end + +""" +Renders a normal vertex array only containing the usual buffers buffers. +""" +function render(vao::GLVertexArray, mode::GLenum=GL_TRIANGLES) + glDrawArrays(mode, 0, length(vao)) + return +end + +""" +Render instanced geometry +""" +renderinstanced(vao::GLVertexArray, a, primitive=GL_TRIANGLES) = renderinstanced(vao, length(a), primitive) + +""" +Renders `amount` instances of an indexed geometry +""" +function renderinstanced(vao::GLVertexArray{GLBuffer{T}}, amount::Integer, primitive=GL_TRIANGLES) where T <: Union{Integer,AbstractFace} + glDrawElementsInstanced(primitive, length(vao.indices) * cardinality(vao.indices), julia2glenum(T), C_NULL, amount) + return +end + +""" +Renders `amount` instances of an not indexed geoemtry geometry +""" +function renderinstanced(vao::GLVertexArray, amount::Integer, primitive=GL_TRIANGLES) + glDrawElementsInstanced(primitive, length(vao), GL_UNSIGNED_INT, C_NULL, amount) + return +end +# handle all uniform objects + +############################################################################################## +# Generic render functions +##### +function enabletransparency() + glEnablei(GL_BLEND, 0) + glDisablei(GL_BLEND, 1) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + return +end diff --git a/GLMakie/src/GLAbstraction/GLRenderObject.jl b/GLMakie/src/GLAbstraction/GLRenderObject.jl new file mode 100644 index 00000000000..962dea14c37 --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLRenderObject.jl @@ -0,0 +1,92 @@ +function RenderObject( + data::Dict{Symbol}, program, pre, + bbs=Node(FRect3D(Vec3f0(0), Vec3f0(1))), + main=nothing + ) + RenderObject(convert(Dict{Symbol,Any}, data), program, pre, bbs, main) +end + +function Base.show(io::IO, obj::RenderObject) + println(io, "RenderObject with ID: ", obj.id) +end + + +Base.getindex(obj::RenderObject, symbol::Symbol) = obj.uniforms[symbol] +Base.setindex!(obj::RenderObject, value, symbol::Symbol) = obj.uniforms[symbol] = value + +Base.getindex(obj::RenderObject, symbol::Symbol, x::Function) = getindex(obj, Val(symbol), x) +Base.getindex(obj::RenderObject, ::Val{:prerender}, x::Function) = obj.prerenderfunctions[x] +Base.getindex(obj::RenderObject, ::Val{:postrender}, x::Function) = obj.postrenderfunctions[x] + +Base.setindex!(obj::RenderObject, value, symbol::Symbol, x::Function) = setindex!(obj, value, Val(symbol), x) +Base.setindex!(obj::RenderObject, value, ::Val{:prerender}, x::Function) = obj.prerenderfunctions[x] = value +Base.setindex!(obj::RenderObject, value, ::Val{:postrender}, x::Function) = obj.postrenderfunctions[x] = value + +const empty_signal = Node(false) +post_empty() = push!(empty_signal, false) + + +""" +Represents standard sets of function applied before rendering +""" +struct StandardPrerender + transparency::Node{Bool} + overdraw::Node{Bool} +end + +function (sp::StandardPrerender)() + if sp.overdraw[] + # Disable depth testing if overdrawing + glDisable(GL_DEPTH_TEST) + else + glEnable(GL_DEPTH_TEST) + glDepthFunc(GL_LEQUAL) + end + # Disable depth write for transparent objects + glDepthMask(sp.transparency[] ? GL_FALSE : GL_TRUE) + # Disable cullface for now, untill all rendering code is corrected! + glDisable(GL_CULL_FACE) + # glCullFace(GL_BACK) + enabletransparency() +end + +struct StandardPostrender + vao::GLVertexArray + primitive::GLenum +end +function (sp::StandardPostrender)() + render(sp.vao, sp.primitive) +end +struct StandardPostrenderInstanced{T} + main::T + vao::GLVertexArray + primitive::GLenum +end +function (sp::StandardPostrenderInstanced)() + renderinstanced(sp.vao, to_value(sp.main), sp.primitive) +end + +struct EmptyPrerender +end +function (sp::EmptyPrerender)() +end +export EmptyPrerender +export prerendertype + +function instanced_renderobject(data, program, bb=Node(FRect3D(Vec3f0(0), Vec3f0(1))), primitive::GLenum=GL_TRIANGLES, main=nothing) + pre = StandardPrerender() + robj = RenderObject(convert(Dict{Symbol,Any}, data), program, pre, nothing, bb, main) + robj.postrenderfunction = StandardPostrenderInstanced(main, robj.vertexarray, primitive) + robj +end + +function std_renderobject(data, program, bb=Node(FRect3D(Vec3f0(0), Vec3f0(1))), primitive=GL_TRIANGLES, main=nothing) + pre = StandardPrerender() + robj = RenderObject(convert(Dict{Symbol,Any}, data), program, pre, nothing, bb, main) + robj.postrenderfunction = StandardPostrender(robj.vertexarray, primitive) + robj +end + +prerendertype(::Type{RenderObject{Pre}}) where {Pre} = Pre +prerendertype(::RenderObject{Pre}) where {Pre} = Pre + diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl new file mode 100644 index 00000000000..d5bc52008e1 --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -0,0 +1,345 @@ +# Different shader string literals- usage: e.g. frag" my shader code" +macro frag_str(source::AbstractString) + quote + ($source, GL_FRAGMENT_SHADER) + end +end +macro vert_str(source::AbstractString) + quote + ($source, GL_VERTEX_SHADER) + end +end +macro geom_str(source::AbstractString) + quote + ($source, GL_GEOMETRY_SHADER) + end +end +macro comp_str(source::AbstractString) + quote + ($source, GL_COMPUTE_SHADER) + end +end + +function getinfolog(obj::GLuint) + # Return the info log for obj, whether it be a shader or a program. + isShader = glIsShader(obj) + getiv = isShader == GL_TRUE ? glGetShaderiv : glGetProgramiv + get_log = isShader == GL_TRUE ? glGetShaderInfoLog : glGetProgramInfoLog + + # Get the maximum possible length for the descriptive error message + maxlength = GLint[0] + getiv(obj, GL_INFO_LOG_LENGTH, maxlength) + maxlength = first(maxlength) + # Return the text of the message if there is any + if maxlength > 0 + buffer = zeros(GLchar, maxlength) + sizei = GLsizei[0] + get_log(obj, maxlength, sizei, buffer) + length = first(sizei) + return unsafe_string(pointer(buffer), length) + else + return "success" + end +end + +function iscompiled(shader::GLuint) + success = GLint[0] + glGetShaderiv(shader, GL_COMPILE_STATUS, success) + return first(success) == GL_TRUE +end +islinked(program::GLuint) = glGetProgramiv(program, GL_LINK_STATUS) == GL_TRUE + +function createshader(shadertype::GLenum) + shaderid = glCreateShader(shadertype) + @assert shaderid > 0 "opengl context is not active or shader type not accepted. Shadertype: $(GLENUM(shadertype).name)" + shaderid::GLuint +end +function createprogram() + program = glCreateProgram() + @assert program > 0 "couldn't create program. Most likely, opengl context is not active" + program::GLuint +end + +shadertype(s::Shader) = s.typ +function shadertype(ext::AbstractString) + ext == ".comp" && return GL_COMPUTE_SHADER + ext == ".vert" && return GL_VERTEX_SHADER + ext == ".frag" && return GL_FRAGMENT_SHADER + ext == ".geom" && return GL_GEOMETRY_SHADER + error("$ext not a valid shader extension") +end + +function uniformlocations(nametypedict::Dict{Symbol, GLenum}, program) + result = Dict{Symbol, Tuple}() + texturetarget = -1 # start -1, as texture samplers start at 0 + for (name, typ) in nametypedict + loc = get_uniform_location(program, name) + str_name = string(name) + if istexturesampler(typ) + texturetarget += 1 + result[name] = (loc, texturetarget) + else + result[name] = (loc,) + end + end + return result +end + +abstract type AbstractLazyShader end +struct LazyShader <: AbstractLazyShader + paths::Tuple + kw_args::Dict{Symbol, Any} + function LazyShader(paths...; kw_args...) + args = Dict{Symbol, Any}(kw_args) + get!(args, :view, Dict{String, String}()) + new(paths, args) + end +end + +gl_convert(shader::GLProgram, data) = shader + +# caching templated shaders is a pain -.- + +# cache for template keys per file +# path --> template keys +const _template_cache = Dict{String, Vector{String}}() +# path --> Dict{template_replacements --> Shader) +const _shader_cache = Dict{String, Dict{Any, Shader}}() +const _program_cache = Dict{Any, GLProgram}() + + +function empty_shader_cache!() + empty!(_template_cache) + empty!(_shader_cache) + empty!(_program_cache) +end + +# TODO remove this silly constructor +function compile_shader(source::Vector{UInt8}, typ, name) + shaderid = createshader(typ) + glShaderSource(shaderid, source) + glCompileShader(shaderid) + if !GLAbstraction.iscompiled(shaderid) + GLAbstraction.print_with_lines(String(source)) + @warn("shader $(name) didn't compile. \n$(GLAbstraction.getinfolog(shaderid))") + end + Shader(name, source, typ, shaderid) +end + +function compile_shader(path, source_str::AbstractString) + typ = shadertype(splitext(path)[2]) + source = Vector{UInt8}(source_str) + name = Symbol(path) + compile_shader(source, typ, name) +end + +function get_shader!(path, template_replacement, view, attributes) + # this should always be in here, since we already have the template keys + shader_dict = _shader_cache[path] + get!(shader_dict, template_replacement) do + template_source = read(path, String) + source = mustache_replace(template_replacement, template_source) + compile_shader(path, source) + end::Shader +end + +function get_template!(path, view, attributes) + get!(_template_cache, path) do + _, ext = splitext(path) + + typ = shadertype(ext) + template_source = read(path, String) + source, replacements = template2source( + template_source, view, attributes + ) + s = compile_shader(path, source) + template_keys = collect(keys(replacements)) + template_replacements = collect(values(replacements)) + # can't yet be in here, since we didn't even have template keys + _shader_cache[path] = Dict(template_replacements => s) + + template_keys + end +end + + +function compile_program(shaders, fragdatalocation) + # Remove old shaders + program = createprogram() + #attach new ones + foreach(shaders) do shader + glAttachShader(program, shader.id) + end + + #Bind frag data + for (location, name) in fragdatalocation + glBindFragDataLocation(program, location, ascii(name)) + end + + #link program + glLinkProgram(program) + if !GLAbstraction.islinked(program) + error( + "program $program not linked. Error in: \n", + join(map(x-> string(x.name), shaders), " or "), "\n", getinfolog(program) + ) + end + # Can be deleted, as they will still be linked to Program and released after program gets released + #foreach(glDeleteShader, shader_ids) + # generate the link locations + nametypedict = uniform_name_type(program) + uniformlocationdict = uniformlocations(nametypedict, program) + GLProgram(program, shaders, nametypedict, uniformlocationdict) +end + +function get_view(kw_dict) + _view = kw_dict[:view] + extension = Sys.isapple() ? "" : "#extension GL_ARB_draw_instanced : enable\n" + _view["GLSL_EXTENSION"] = extension*get(_view, "GLSL_EXTENSIONS", "") + _view["GLSL_VERSION"] = glsl_version_string() + _view +end + +function gl_convert(lazyshader::AbstractLazyShader, data) + kw_dict = lazyshader.kw_args + paths = lazyshader.paths + if all(x-> isa(x, Shader), paths) + fragdatalocation = get(kw_dict, :fragdatalocation, Tuple{Int, String}[]) + return compile_program([paths...], fragdatalocation) + end + v = get_view(kw_dict) + fragdatalocation = get(kw_dict, :fragdatalocation, Tuple{Int, String}[]) + + # Tuple(Source, ShaderType) + if all(paths) do x + isa(x, Tuple) && length(x) == 2 && + isa(first(x), String) && + isa(last(x), GLenum) + end + # we don't cache view & templates for shader strings! + shaders = map(paths) do source_typ + source, typ = source_typ + src, _ = template2source(source, v, data) + compile_shader(Vector{UInt8}(src), typ, :from_string) + end + return compile_program([shaders...], fragdatalocation) + end + if !all(x-> isa(x, String), paths) + error("Please supply only paths or tuples of (source, typ) for Lazy Shader + Found: $paths" + ) + end + template_keys = Vector{Vector{String}}(undef, length(paths)) + replacements = Vector{Vector{String}}(undef, length(paths)) + for (i, path) in enumerate(paths) + template = get_template!(path, v, data) + template_keys[i] = template + replacements[i] = String[mustache2replacement(t, v, data) for t in template] + end + program = get!(_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)) + for (i, path) in enumerate(paths) + tr = Dict(zip(template_keys[i], replacements[i])) + shaders[i] = get_shader!(path, tr, v, data) + end + compile_program(shaders, fragdatalocation) + end +end + + +function insert_from_view(io, replace_view::Function, keyword::AbstractString) + print(io, replace_view(keyword)) + nothing +end + +function insert_from_view(io, replace_view::Dict, keyword::AbstractString) + if haskey(replace_view, keyword) + print(io, replace_view[keyword]) + end + nothing +end +""" +Replaces +{{keyword}} with the key in `replace_view`, or replace_view(key) +in a string +""" +function mustache_replace(replace_view::Union{Dict, Function}, string) + io = IOBuffer() + replace_started = false + open_mustaches = 0 + closed_mustaches = 0 + i = 0 + replace_begin = i + last_char = SubString(string, 1, 1) + len = lastindex(string) + while i <= len + i = nextind(string, i) + i > len && break + char = string[i] + if replace_started + # ignore, or wait for } + if char == '}' + closed_mustaches += 1 + if closed_mustaches == 2 # we found a complete mustache! + insert_from_view(io, replace_view, SubString(string, replace_begin+1, i-2)) + open_mustaches = 0 + closed_mustaches = 0 + replace_started = false + end + else + closed_mustaches = 0 + continue + end + elseif char == '{' + open_mustaches += 1 + if open_mustaches == 2 + replace_begin = i + replace_started = true + end + else + if open_mustaches == 1 + print(io, last_char) + end + print(io, char) # just copy all the rest + open_mustaches = 0 + closed_mustaches = 0 + end + last_char = char + end + String(take!(io)) +end + + +function mustache2replacement(mustache_key, view, attributes) + haskey(view, mustache_key) && return view[mustache_key] + for postfix in ("_type", "_calculation") + keystring = replace(mustache_key, postfix => "") + keysym = Symbol(keystring) + if haskey(attributes, keysym) + val = attributes[keysym] + if !isa(val, AbstractString) + if postfix == "_type" + return toglsltype_string(val)::String + else postfix == "_calculation" + return glsl_variable_access(keystring, val) + end + end + end + end + return "" + # error("No match found: $(mustache_key)") +end + +# Takes a shader template and renders the template and returns shader source +template2source(source::Vector{UInt8}, view, attributes::Dict{Symbol, Any}) = template2source(String(source), attributes, view) +function template2source(source::AbstractString, view, attributes::Dict{Symbol, Any}) + replacements = Dict{String, String}() + source = mustache_replace(source) do mustache_key + r = mustache2replacement(mustache_key, view, attributes) + replacements[mustache_key] = r + return r + end + return source, replacements +end diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl new file mode 100644 index 00000000000..7d776f246ab --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -0,0 +1,533 @@ +struct TextureParameters{NDim} + minfilter::Symbol + magfilter::Symbol # magnification + repeat ::NTuple{NDim, Symbol} + anisotropic::Float32 + swizzle_mask::Vector{GLenum} +end + +abstract type OpenglTexture{T, NDIM} <: GPUArray{T, NDIM} end + +mutable struct Texture{T <: GLArrayEltypes, NDIM} <: OpenglTexture{T, NDIM} + id ::GLuint + texturetype ::GLenum + pixeltype ::GLenum + internalformat ::GLenum + format ::GLenum + parameters ::TextureParameters{NDIM} + size ::NTuple{NDIM, Int} + context ::GLContext + function Texture{T, NDIM}( + id ::GLuint, + texturetype ::GLenum, + pixeltype ::GLenum, + internalformat ::GLenum, + format ::GLenum, + parameters ::TextureParameters{NDIM}, + size ::NTuple{NDIM, Int} + ) where {T, NDIM} + tex = new( + id, + texturetype, + pixeltype, + internalformat, + format, + parameters, + size, + current_context() + ) + finalizer(free, tex) + tex + end +end + +# for bufferSampler, aka Texture Buffer +mutable struct TextureBuffer{T <: GLArrayEltypes} <: OpenglTexture{T, 1} + texture::Texture{T, 1} + buffer::GLBuffer{T} +end +Base.size(t::TextureBuffer) = size(t.buffer) +Base.size(t::TextureBuffer, i::Integer) = size(t.buffer, i) +Base.length(t::TextureBuffer) = length(t.buffer) +bind(t::Texture) = glBindTexture(t.texturetype, t.id) +bind(t::Texture, id) = glBindTexture(t.texturetype, id) + +is_texturearray(t::Texture) = t.texturetype == GL_TEXTURE_2D_ARRAY +is_texturebuffer(t::Texture) = t.texturetype == GL_TEXTURE_BUFFER + +colordim(::Type{T}) where {T} = cardinality(T) +colordim(::Type{T}) where {T <: Real} = 1 + +function set_packing_alignment(a) # at some point we should specialize to array/ptr a + glPixelStorei(GL_UNPACK_ALIGNMENT, 1) + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0) + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0) + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0) +end + +function Texture( + data::Ptr{T}, dims::NTuple{NDim, Int}; + internalformat::GLenum = default_internalcolorformat(T), + texturetype ::GLenum = default_texturetype(NDim), + format ::GLenum = default_colorformat(T), + mipmap = false, + parameters... # rest should be texture parameters + ) where {T, NDim} + texparams = TextureParameters(T, NDim; parameters...) + id = glGenTextures() + glBindTexture(texturetype, id) + set_packing_alignment(data) + numbertype = julia2glenum(eltype(T)) + glTexImage(texturetype, 0, internalformat, dims..., 0, format, numbertype, data) + mipmap && glGenerateMipmap(texturetype) + texture = Texture{T, NDim}( + id, texturetype, numbertype, internalformat, format, + texparams, + dims + ) + set_parameters(texture) + texture::Texture{T, NDim} +end +export resize_nocopy! +function resize_nocopy!(t::Texture{T, ND}, newdims::NTuple{ND, Int}) where {T, ND} + bind(t) + glTexImage(t.texturetype, 0, t.internalformat, newdims..., 0, t.format, t.pixeltype, C_NULL) + t.size = newdims + bind(t, 0) + t +end + +""" +Constructor for empty initialization with NULL pointer instead of an array with data. +You just need to pass the wanted color/vector type and the dimensions. +To which values the texture gets initialized is driver dependent +""" +Texture(::Type{T}, dims::NTuple{N, Int}; kw_args...) where {T <: GLArrayEltypes, N} = + Texture(convert(Ptr{T}, C_NULL), dims; kw_args...)::Texture{T, N} + +""" +Constructor for a normal array, with color or Abstract Arrays as elements. +So Array{Real, 2} == Texture2D with 1D Colorant dimension +Array{Vec1/2/3/4, 2} == Texture2D with 1/2/3/4D Colorant dimension +Colors from Colors.jl should mostly work as well +""" +Texture(image::Array{T, NDim}; kw_args...) where {T <: GLArrayEltypes, NDim} = + Texture(pointer(image), size(image); kw_args...)::Texture{T, NDim} + +function Texture(s::ShaderAbstractions.Sampler{T, N}; kwargs...) where {T, N} + tex = Texture( + pointer(s.data), size(s.data), + minfilter = s.minfilter, magfilter = s.magfilter, + x_repeat = s.repeat[1], y_repeat = s.repeat[min(2, N)], z_repeat = s.repeat[min(3, N)], + anisotropic = s.anisotropic; kwargs... + ) + ShaderAbstractions.connect!(s, tex) + return tex +end + +""" +Constructor for Array Texture +""" +function Texture( + data::Vector{Array{T, 2}}; + internalformat::GLenum = default_internalcolorformat(T), + texturetype::GLenum = GL_TEXTURE_2D_ARRAY, + format::GLenum = default_colorformat(T), + parameters... + ) where T <: GLArrayEltypes + texparams = TextureParameters(T, 2; parameters...) + id = glGenTextures() + + glBindTexture(texturetype, id) + + numbertype = julia2glenum(eltype(T)) + + layers = length(data) + dims = map(size, data) + maxdims = foldl(dims, init = (0,0)) do v0, x + a = max(v0[1], x[1]) + b = max(v0[2], x[2]) + (a,b) + end + set_packing_alignment(data) + glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, internalformat, maxdims..., layers) + for (layer, texel) in enumerate(data) + width, height = size(texel) + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer-1, width, height, 1, format, numbertype, texel) + end + + texture = Texture{T, 2}( + id, texturetype, numbertype, + internalformat, format, texparams, + tuple(maxdims...) + ) + set_parameters(texture) + texture +end + + + +function TextureBuffer(buffer::GLBuffer{T}) where T <: GLArrayEltypes + texture_type = GL_TEXTURE_BUFFER + id = glGenTextures() + glBindTexture(texture_type, id) + internalformat = default_internalcolorformat(T) + glTexBuffer(texture_type, internalformat, buffer.id) + tex = Texture{T, 1}( + id, texture_type, julia2glenum(T), internalformat, + default_colorformat(T), TextureParameters(T, 1), + size(buffer) + ) + TextureBuffer(tex, buffer) +end +function TextureBuffer(buffer::Vector{T}) where T <: GLArrayEltypes + buff = GLBuffer(buffer, buffertype = GL_TEXTURE_BUFFER, usage = GL_DYNAMIC_DRAW) + TextureBuffer(buff) +end + +#= +Some special treatmend for types, with alpha in the First place + +function Texture{T <: Real, NDim}(image::Array{ARGB{T}, NDim}, texture_properties::Vector{(Symbol, Any)}) + data = map(image) do colorvalue + AlphaColorValue(colorvalue.c, colorvalue.alpha) + end + Texture(pointer(data), [size(data)...], texture_properties) +end +=# + +#= +Creates a texture from an Image +=# +##function Texture(image::Image, texture_properties::Vector{(Symbol, Any)}) +# data = image.data +# Texture(mapslices(reverse, data, ndims(data)), texture_properties) +#end + + +GeometryBasics.width(t::Texture) = size(t, 1) +GeometryBasics.height(t::Texture) = size(t, 2) +depth(t::Texture) = size(t, 3) + +# AbstractArrays default show assumes `getindex`. Try to catch all calls +# https://discourse.julialang.org/t/overload-show-for-array-of-custom-types/9589 + +Base.show(io::IO, t::Texture) = show(IOContext(io), MIME"text/plain"(), t) + +function Base.show(io::IOContext, mime::MIME"text/plain", t::Texture{T,D}) where {T,D} + if get(io, :compact, false) + println(io, "Texture$(D)D, ID: $(t.id), Size: $(size(t))") + else + show(io.io, mime, t) + end +end + +function Base.show(io::IO, ::MIME"text/plain", t::Texture{T,D}) where {T,D} + println(io, "Texture$(D)D: ") + println(io, " ID: ", t.id) + println(io, " Size: ", reduce(size(t), init = "Dimensions: ") do v0, v1 + v0*"x"*string(v1) + end) + println(io, " Julia pixel type: ", T) + println(io, " OpenGL pixel type: ", GLENUM(t.pixeltype).name) + println(io, " Format: ", GLENUM(t.format).name) + println(io, " Internal format: ", GLENUM(t.internalformat).name) + println(io, " Parameters: ", t.parameters) +end + +# GPUArray interface: +function unsafe_copy!(a::Vector{T}, readoffset::Int, b::TextureBuffer{T}, writeoffset::Int, len::Int) where T + copy!(a, readoffset, b.buffer, writeoffset, len) + glBindTexture(b.texture.texturetype, b.texture.id) + glTexBuffer(b.texture.texturetype, b.texture.internalformat, b.buffer.id) # update texture +end + +function unsafe_copy!(a::TextureBuffer{T}, readoffset::Int, b::Vector{T}, writeoffset::Int, len::Int) where T + copy!(a.buffer, readoffset, b, writeoffset, len) + glBindTexture(a.texture.texturetype, a.texture.id) + glTexBuffer(a.texture.texturetype, a.texture.internalformat, a.buffer.id) # update texture +end + +function unsafe_copy!(a::TextureBuffer{T}, readoffset::Int, b::TextureBuffer{T}, writeoffset::Int, len::Int) where T + unsafe_copy!(a.buffer, readoffset, b.buffer, writeoffset, len) + + glBindTexture(a.texture.texturetype, a.texture.id) + glTexBuffer(a.texture.texturetype, a.texture.internalformat, a.buffer.id) # update texture + + glBindTexture(b.texture.texturetype, btexture..id) + glTexBuffer(b.texture.texturetype, b.texture.internalformat, b.buffer.id) # update texture + glBindTexture(t.texture.texturetype, 0) +end +function gpu_setindex!(t::TextureBuffer{T}, newvalue::Vector{T}, indexes::UnitRange{I}) where {T, I <: Integer} + glBindTexture(t.texture.texturetype, t.texture.id) + t.buffer[indexes] = newvalue # set buffer indexes + glTexBuffer(t.texture.texturetype, t.texture.internalformat, t.buffer.id) # update texture + glBindTexture(t.texture.texturetype, 0) +end +function gpu_setindex!(t::Texture{T, 1}, newvalue::Array{T, 1}, indexes::UnitRange{I}) where {T, I <: Integer} + glBindTexture(t.texturetype, t.id) + texsubimage(t, newvalue, indexes) + glBindTexture(t.texturetype, 0) +end +function gpu_setindex!(t::Texture{T, N}, newvalue::Array{T, N}, indexes::Union{UnitRange,Integer}...) where {T, N} + glBindTexture(t.texturetype, t.id) + texsubimage(t, newvalue, indexes...) + glBindTexture(t.texturetype, 0) +end + + +function gpu_setindex!(target::Texture{T, 2}, source::Texture{T, 2}, fbo=glGenFramebuffers()) where T + glBindFramebuffer(GL_FRAMEBUFFER, fbo) + glFramebufferTexture2D( + GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, source.id, 0 + ) + glFramebufferTexture2D( + GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, + GL_TEXTURE_2D, target.id, 0 + ) + glDrawBuffer(GL_COLOR_ATTACHMENT1); + w, h = map(minimum, zip(size(target), size(source))) + glBlitFramebuffer( + 0, 0, w, h, 0, 0, w, h, + GL_COLOR_BUFFER_BIT, GL_NEAREST + ) +end + + + +#= +function gpu_setindex!{T}(target::Texture{T, 2}, source::Texture{T, 2}, fbo=glGenFramebuffers()) + w, h = map(minimum, zip(size(target), size(source))) + glCopyImageSubData( source.id, source.texturetype, + 0,0,0,0, + target.id, target.texturetype, + 0,0,0,0, w,h,0); +end +=# +# Implementing the GPUArray interface +function gpu_data(t::Texture{T, ND}) where {T, ND} + result = Array{T, ND}(undef, size(t)) + unsafe_copy!(result, t) + return result +end + +function unsafe_copy!(dest::Array{T, N}, source::Texture{T, N}) where {T,N} + bind(source) + glGetTexImage(source.texturetype, 0, source.format, source.pixeltype, dest) + bind(source, 0) + nothing +end + +gpu_data(t::TextureBuffer{T}) where {T} = gpu_data(t.buffer) +gpu_getindex(t::TextureBuffer{T}, i::UnitRange{Int64}) where {T} = t.buffer[i] + +similar(t::Texture{T, NDim}, newdims::Int...) where {T, NDim} = similar(t, newdims) +function similar(t::TextureBuffer{T}, newdims::NTuple{1, Int}) where T + buff = similar(t.buffer, newdims...) + return TextureBuffer(buff) +end +function similar(t::Texture{T, NDim}, newdims::NTuple{NDim, Int}) where {T, NDim} + Texture( + Ptr{T}(C_NULL), + newdims, t.texturetype, + t.pixeltype, + t.internalformat, + t.format, + t.parameters + ) +end +# Resize Texture +function gpu_resize!(t::TextureBuffer{T}, newdims::NTuple{1, Int}) where T + resize!(t.buffer, newdims) + glBindTexture(t.texture.texturetype, t.texture.id) + glTexBuffer(t.texture.texturetype, t.texture.internalformat, t.buffer.id) #update data in texture + t.texture.size = newdims + glBindTexture(t.texture.texturetype, 0) + t +end +# Resize Texture +function gpu_resize!(t::Texture{T, ND}, newdims::NTuple{ND, Int}) where {T, ND} + # dangerous code right here...Better write a few tests for this + newtex = similar(t, newdims) + old_size = size(t) + gpu_setindex!(newtex, t) + t.size = newdims + free(t) + t.id = newtex.id + return t +end + +texsubimage(t::Texture{T, 1}, newvalue::Array{T, 1}, xrange::UnitRange, level=0) where {T} = glTexSubImage1D( + t.texturetype, level, first(xrange)-1, length(xrange), t.format, t.pixeltype, newvalue +) +function texsubimage(t::Texture{T, 2}, newvalue::Array{T, 2}, xrange::UnitRange, yrange::UnitRange, level=0) where T + glTexSubImage2D( + t.texturetype, level, + first(xrange)-1, first(yrange)-1, length(xrange), length(yrange), + t.format, t.pixeltype, newvalue + ) +end +texsubimage(t::Texture{T, 3}, newvalue::Array{T, 3}, xrange::UnitRange, yrange::UnitRange, zrange::UnitRange, level=0) where {T} = glTexSubImage3D( + t.texturetype, level, + first(xrange)-1, first(yrange)-1, first(zrange)-1, length(xrange), length(yrange), length(zrange), + t.format, t.pixeltype, newvalue +) + + +Base.iterate(t::TextureBuffer{T}) where {T} = iterate(t.buffer) +function Base.iterate(t::TextureBuffer{T}, state::Tuple{Ptr{T}, Int}) where T + v_idx = iterate(t.buffer, state) + if v_idx === nothing + glBindTexture(t.texturetype, t.id) + glTexBuffer(t.texturetype, t.internalformat, t.buffer.id) + glBindTexture(t.texturetype, 0) + end + v_idx +end +function default_colorformat_sym(colordim::Integer, isinteger::Bool, colororder::AbstractString) + colordim > 4 && error("no colors with dimension > 4 allowed. Dimension given: ", colordim) + sym = "GL_" + # Handle that colordim == 1 => RED instead of R + color = colordim == 1 ? "RED" : colororder[1:colordim] + # Handle gray value + integer = isinteger ? "_INTEGER" : "" + sym *= color * integer + return Symbol(sym) +end + +default_colorformat_sym(::Type{T}) where {T <: Real} = default_colorformat_sym(1, T <: Integer, "RED") +default_colorformat_sym(::Type{T}) where {T <: AbstractArray} = default_colorformat_sym(cardinality(T), eltype(T) <: Integer, "RGBA") +default_colorformat_sym(::Type{T}) where {T <: StaticVector} = default_colorformat_sym(cardinality(T), eltype(T) <: Integer, "RGBA") +default_colorformat_sym(::Type{T}) where {T <: Colorant} = default_colorformat_sym(cardinality(T), eltype(T) <: Integer, string(Base.typename(T).name)) + +@generated function default_colorformat(::Type{T}) where T + sym = default_colorformat_sym(T) + if !isdefined(ModernGL, sym) + error("$T doesn't have a propper mapping to an OpenGL format") + end + :($sym) +end + +function default_internalcolorformat_sym(::Type{T}) where T + cdim = colordim(T) + if cdim > 4 || cdim < 1 + error("$(cdim)-dimensional colors not supported") + end + eltyp = eltype(T) + sym = "GL_" + sym *= "RGBA"[1:cdim] + bits = sizeof(eltyp) * 8 + sym *= bits <= 32 ? string(bits) : error("$(T) has too many bits") + if eltyp <: AbstractFloat + sym *= "F" + elseif eltyp <: FixedPoint + sym *= eltyp <: Normed ? "" : "_SNORM" + elseif eltyp <: Signed + sym *= "I" + elseif eltyp <: Unsigned + sym *= "UI" + end + Symbol(sym) +end + +# for I = 1:4 +# for +@generated function default_internalcolorformat(::Type{T}) where T + sym = default_internalcolorformat_sym(T) + if !isdefined(ModernGL, sym) + error("$T doesn't have a propper mapping to an OpenGL format") + end + :($sym) +end + + +#Supported texture modes/dimensions +function default_texturetype(ndim::Integer) + ndim == 1 && return GL_TEXTURE_1D + ndim == 2 && return GL_TEXTURE_2D + ndim == 3 && return GL_TEXTURE_3D + error("Dimensionality: $(ndim), not supported for OpenGL texture") +end + + +map_texture_paramers(s::NTuple{N, Symbol}) where {N} = map(map_texture_paramers, s) + +function map_texture_paramers(s::Symbol) + + s == :clamp_to_edge && return GL_CLAMP_TO_EDGE + s == :mirrored_repeat && return GL_MIRRORED_REPEAT + s == :repeat && return GL_REPEAT + + s == :linear && return GL_LINEAR + s == :nearest && return GL_NEAREST + s == :nearest_mipmap_nearest && return GL_NEAREST_MIPMAP_NEAREST + s == :linear_mipmap_nearest && return GL_LINEAR_MIPMAP_NEAREST + s == :nearest_mipmap_linear && return GL_NEAREST_MIPMAP_LINEAR + s == :linear_mipmap_linear && return GL_LINEAR_MIPMAP_LINEAR + + error("$s is not a valid texture parameter") +end + +function TextureParameters(T, NDim; + minfilter = T <: Integer ? :nearest : :linear, + magfilter = minfilter, # magnification + x_repeat = :clamp_to_edge, #wrap_s + y_repeat = x_repeat, #wrap_t + z_repeat = x_repeat, #wrap_r + anisotropic = 1f0 + ) + T <: Integer && (minfilter == :linear || magfilter == :linear) && error("Wrong Texture Parameter: Integer texture can't interpolate. Try :nearest") + repeat = (x_repeat, y_repeat, z_repeat) + swizzle_mask = if T <: Gray + GLenum[GL_RED, GL_RED, GL_RED, GL_ONE] + elseif T <: GrayA + GLenum[GL_RED, GL_RED, GL_RED, GL_ALPHA] + else + GLenum[] + end + TextureParameters( + minfilter, magfilter, ntuple(i->repeat[i], NDim), + anisotropic, swizzle_mask + ) +end +function TextureParameters(t::Texture{T, NDim}; kw_args...) where {T, NDim} + TextureParameters(T, NDim; kw_args...) +end + +const GL_TEXTURE_MAX_ANISOTROPY_EXT = GLenum(0x84FE) + +function set_parameters(t::Texture{T, N}, params::TextureParameters=t.parameters) where {T, N} + fnames = (:minfilter, :magfilter, :repeat) + data = Dict([(name, map_texture_paramers(getfield(params, name))) for name in fnames]) + result = Tuple{GLenum, Any}[] + push!(result, (GL_TEXTURE_MIN_FILTER, data[:minfilter])) + push!(result, (GL_TEXTURE_MAG_FILTER, data[:magfilter])) + push!(result, (GL_TEXTURE_WRAP_S, data[:repeat][1])) + if !isempty(params.swizzle_mask) + push!(result, (GL_TEXTURE_SWIZZLE_RGBA, params.swizzle_mask)) + end + N >= 2 && push!(result, (GL_TEXTURE_WRAP_T, data[:repeat][2])) + if N >= 3 && !is_texturearray(t) # for texture arrays, third dimension can not be set + push!(result, (GL_TEXTURE_WRAP_R, data[:repeat][3])) + end + push!(result, (GL_TEXTURE_MAX_ANISOTROPY_EXT, params.anisotropic)) + t.parameters = params + set_parameters(t, result) +end +function texparameter(t::Texture, key::GLenum, val::GLenum) + glTexParameteri(t.texturetype, key, val) +end +function texparameter(t::Texture, key::GLenum, val::Vector) + glTexParameteriv(t.texturetype, key, val) +end +function texparameter(t::Texture, key::GLenum, val::Float32) + glTexParameterf(t.texturetype, key, val) +end +function set_parameters(t::Texture, parameters::Vector{Tuple{GLenum, Any}}) + bind(t) + for elem in parameters + texparameter(t, elem...) + end + bind(t, 0) +end diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl new file mode 100644 index 00000000000..1ca49d54117 --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -0,0 +1,397 @@ +############################################################################ +const TOrSignal{T} = Union{Node{T},T} + +const ArrayOrSignal{T,N} = TOrSignal{X} where X <: AbstractArray{T,N} +const VecOrSignal{T} = ArrayOrSignal{T,1} +const MatOrSignal{T} = ArrayOrSignal{T,2} +const VolumeOrSignal{T} = ArrayOrSignal{T,3} + +const ArrayTypes{T,N} = Union{GPUArray{T,N},ArrayOrSignal{T,N}} +const VectorTypes{T} = ArrayTypes{T,1} +const MatTypes{T} = ArrayTypes{T,2} +const VolumeTypes{T} = ArrayTypes{T,3} + +@enum Projection PERSPECTIVE ORTHOGRAPHIC +@enum MouseButton MOUSE_LEFT MOUSE_MIDDLE MOUSE_RIGHT + +""" +Returns the cardinality of a type. falls back to length +""" +cardinality(x) = length(x) +cardinality(x::Number) = 1 +cardinality(x::Type{T}) where {T <: Number} = 1 + +struct Shader + name::Symbol + source::Vector{UInt8} + typ::GLenum + id::GLuint + context::GLContext + function Shader(name, source, typ, id) + new(name, source, typ, id, current_context()) + end +end + +function Shader(name, source::Vector{UInt8}, typ) + compile_shader(source, typ, name) +end + +name(s::Shader) = s.name + +import Base: == + +function (==)(a::Shader, b::Shader) + a.source == b.source && a.typ == b.typ && a.id == b.id && a.context == b.context +end + +function Base.hash(s::Shader, h::UInt64) + hash((s.source, s.typ, s.id, s.context), h) +end + + +function Base.show(io::IO, shader::Shader) + println(io, GLENUM(shader.typ).name, " shader: $(shader.name))") + println(io, "source:") + print_with_lines(io, String(shader.source)) +end + +mutable struct GLProgram + id::GLuint + shader::Vector{Shader} + nametype::Dict{Symbol,GLenum} + uniformloc::Dict{Symbol,Tuple} + context::GLContext + function GLProgram(id::GLuint, shader::Vector{Shader}, nametype::Dict{Symbol,GLenum}, uniformloc::Dict{Symbol,Tuple}) + obj = new(id, shader, nametype, uniformloc, current_context()) + finalizer(free, obj) + obj + end +end + +function Base.show(io::IO, p::GLProgram) + println(io, "GLProgram: $(p.id)") + println(io, "Shaders:") + for shader in p.shader + println(io, shader) + end + println(io, "uniforms:") + for (name, typ) in p.nametype + println(io, " ", name, "::", GLENUM(typ).name) + end +end + +############################################ +# Framebuffers and the like + +struct RenderBuffer + id::GLuint + format::GLenum + context::GLContext + function RenderBuffer(format, dimension) + @assert length(dimensions) == 2 + id = GLuint[0] + glGenRenderbuffers(1, id) + glBindRenderbuffer(GL_RENDERBUFFER, id[1]) + glRenderbufferStorage(GL_RENDERBUFFER, format, dimension...) + new(id, format, current_context()) + end +end + +function resize!(rb::RenderBuffer, newsize::AbstractArray) + if length(newsize) != 2 + error("RenderBuffer needs to be 2 dimensional. Dimension found: ", newsize) + end + glBindRenderbuffer(GL_RENDERBUFFER, rb.id) + glRenderbufferStorage(GL_RENDERBUFFER, rb.format, newsize...) +end + +struct FrameBuffer{T} + id::GLuint + attachments::Vector{Any} + context::GLContext + function FrameBuffer{T}(dimensions::Node) where T + fb = glGenFramebuffers() + glBindFramebuffer(GL_FRAMEBUFFER, fb) + new(id, attachments, current_context()) + end +end + +function resize!(fbo::FrameBuffer, newsize::AbstractArray) + if length(newsize) != 2 + error("FrameBuffer needs to be 2 dimensional. Dimension found: ", newsize) + end + for elem in fbo.attachments + resize!(elem) + end +end + +######################################################################################## +# OpenGL Arrays + +const GLArrayEltypes = Union{StaticVector,Real,Colorant} +""" +Transform julia datatypes to opengl enum type +""" +julia2glenum(x::Type{T}) where {T <: FixedPoint} = julia2glenum(FixedPointNumbers.rawtype(x)) +julia2glenum(x::Union{Type{T},T}) where {T <: Union{StaticVector,Colorant}} = julia2glenum(eltype(x)) +julia2glenum(::Type{OffsetInteger{O,T}}) where {O,T} = julia2glenum(T) +julia2glenum(::Type{GLubyte}) = GL_UNSIGNED_BYTE +julia2glenum(::Type{GLbyte}) = GL_BYTE +julia2glenum(::Type{GLuint}) = GL_UNSIGNED_INT +julia2glenum(::Type{GLushort}) = GL_UNSIGNED_SHORT +julia2glenum(::Type{GLshort}) = GL_SHORT +julia2glenum(::Type{GLint}) = GL_INT +julia2glenum(::Type{GLfloat}) = GL_FLOAT +julia2glenum(::Type{GLdouble}) = GL_DOUBLE +julia2glenum(::Type{Float16}) = GL_HALF_FLOAT + +struct DepthStencil_24_8 <: Real + data::NTuple{4,UInt8} +end + +Base.eltype(::Type{<: DepthStencil_24_8}) = DepthStencil_24_8 +julia2glenum(x::Type{DepthStencil_24_8}) = GL_UNSIGNED_INT_24_8 + +function julia2glenum(::Type{T}) where T + error("Type: $T not supported as opengl number datatype") +end + +include("GLBuffer.jl") +include("GLTexture.jl") + +######################################################################## + +""" +Represents an OpenGL vertex array type. +Can be created from a dict of buffers and an opengl Program. +Keys with the name `indices` will get special treatment and will be used as +the indexbuffer. +""" +mutable struct GLVertexArray{T} + program::GLProgram + id::GLuint + bufferlength::Int + buffers::Dict{String,GLBuffer} + indices::T + context::GLContext + + function GLVertexArray{T}(program, id, bufferlength, buffers, indices) where T + new(program, id, bufferlength, buffers, indices, current_context()) + end +end + +""" +returns the length of the vertex array. +This is amount of primitives stored in the vertex array, needed for `glDrawArrays` +""" +length(vao::GLVertexArray) = length(first(vao.buffers)[2]) # all buffers have same length, so first should do! + +GLVertexArray(vao::GLVertexArray) = GLVertexArray(vao.buffers, vao.program) + +function GLVertexArray(bufferdict::Dict, program::GLProgram) + # get the size of the first array, to assert later, that all have the same size + indexes = -1 + len = -1 + id = glGenVertexArrays() + glBindVertexArray(id) + lenbuffer = 0 + buffers = Dict{String,GLBuffer}() + for (name, buffer) in bufferdict + if isa(buffer, GLBuffer) && buffer.buffertype == GL_ELEMENT_ARRAY_BUFFER + bind(buffer) + indexes = buffer + elseif Symbol(name) == :indices + indexes = buffer + else + attribute = string(name) + len == -1 && (len = length(buffer)) + # TODO: use glVertexAttribDivisor to allow multiples of the longest buffer + if len != length(buffer) + # We don't know which buffer has the wrong size, so list all of them + bufferlengths = "" + for (name, buffer) in bufferdict + if isa(buffer, GLBuffer) && buffer.buffertype == GL_ELEMENT_ARRAY_BUFFER + elseif Symbol(name) == :indices + else + bufferlengths *= "\n\t$name has length $(length(buffer))" + end + end + error( + "Buffer $attribute does not have the same length as the other buffers." * + bufferlengths + ) + end + bind(buffer) + attribLocation = get_attribute_location(program.id, attribute) + (attribLocation == -1) && continue + glVertexAttribPointer(attribLocation, cardinality(buffer), julia2glenum(eltype(buffer)), GL_FALSE, 0, C_NULL) + glEnableVertexAttribArray(attribLocation) + buffers[attribute] = buffer + lenbuffer = buffer + end + end + glBindVertexArray(0) + if indexes == -1 + indexes = len + end + obj = GLVertexArray{typeof(indexes)}(program, id, len, buffers, indexes) + finalizer(free, obj) + return obj +end +using ShaderAbstractions: Buffer +function GLVertexArray(program::GLProgram, buffers::Buffer, triangles::AbstractVector{<: GLTriangleFace}) + # get the size of the first array, to assert later, that all have the same size + id = glGenVertexArrays() + glBindVertexArray(id) + for property_name in propertynames(buffers) + array = getproperty(buffers, property_name) + attribute = string(property_name) + # TODO: use glVertexAttribDivisor to allow multiples of the longest buffer + buffer = GLBuffer(array) + bind(buffer) + attribLocation = get_attribute_location(program.id, attribute) + if attribLocation == -1 + error("could not bind attribute $(attribute)") + end + glVertexAttribPointer(attribLocation, cardinality(buffer), julia2glenum(eltype(buffer)), GL_FALSE, 0, C_NULL) + glEnableVertexAttribArray(attribLocation) + buffers[attribute] = buffer + end + glBindVertexArray(0) + indices = indexbuffer(triangles) + obj = GLVertexArray{typeof(indexes)}(program, id, len, buffers, indices) + finalizer(free, obj) + return obj +end + +function Base.show(io::IO, vao::GLVertexArray) + show(io, vao.program) + println(io, "GLVertexArray $(vao.id):") + print(io, "GLVertexArray $(vao.id) buffers: ") + writemime(io, MIME("text/plain"), vao.buffers) + println(io, "\nGLVertexArray $(vao.id) indices: ", vao.indices) +end + + +################################################################################## + +const RENDER_OBJECT_ID_COUNTER = Ref(zero(GLushort)) + +mutable struct RenderObject{Pre} + main # main object + uniforms::Dict{Symbol,Any} + vertexarray::GLVertexArray + prerenderfunction::Pre + postrenderfunction + id::GLushort + boundingbox # workaround for having lazy boundingbox queries, while not using multiple dispatch for boundingbox function (No type hierarchy for RenderObjects) + function RenderObject{Pre}( + main, uniforms::Dict{Symbol,Any}, vertexarray::GLVertexArray, + prerenderfunctions, postrenderfunctions, + boundingbox + ) where Pre + RENDER_OBJECT_ID_COUNTER[] += one(GLushort) + new( + main, uniforms, vertexarray, + prerenderfunctions, postrenderfunctions, + RENDER_OBJECT_ID_COUNTER[], boundingbox + ) + end +end + + +function RenderObject( + data::Dict{Symbol,Any}, program, + pre::Pre, post, + bbs=Node(FRect3D(Vec3f0(0), Vec3f0(1))), + main=nothing + ) where Pre + targets = get(data, :gl_convert_targets, Dict()) + delete!(data, :gl_convert_targets) + passthrough = Dict{Symbol,Any}() # we also save a few non opengl related values in data + for (k, v) in data # convert everything to OpenGL compatible types + if haskey(targets, k) + # glconvert is designed to just convert everything to a fitting opengl datatype, but sometimes exceptions are needed + # e.g. Texture{T,1} and GLBuffer{T} are both usable as an native conversion canditate for a Julia's Array{T, 1} type. + # but in some cases we want a Texture, sometimes a GLBuffer or TextureBuffer + data[k] = gl_convert(targets[k], v) + else + k in (:indices, :visible, :fxaa, :ssao, :label, :cycle) && continue + # structs are treated differently, since they have to be composed into their fields + if isa_gl_struct(v) + merge!(data, gl_convert_struct(v, k)) + elseif applicable(gl_convert, v) # if can't be converted to an OpenGL datatype, + data[k] = gl_convert(v) + else # put it in passthrough + delete!(data, k) + passthrough[k] = v + end + end + end + # handle meshes seperately, since they need expansion + meshs = filter(((key, value),) -> isa(value, NativeMesh), data) + + if !isempty(meshs) + merge!(data, [v.data for (k, v) in meshs]...) + end + buffers = filter(((key, value),) -> isa(value, GLBuffer) || key == :indices, data) + uniforms = filter(((key, value),) -> !isa(value, GLBuffer) && key != :indices, data) + get!(data, :visible, true) # make sure, visibility is set + merge!(data, passthrough) # in the end, we insert back the non opengl data, to keep things simple + p = gl_convert(to_value(program), data) # "compile" lazyshader + vertexarray = GLVertexArray(Dict(buffers), p) + robj = RenderObject{Pre}( + main, + data, + vertexarray, + pre, + post, + bbs + ) + # automatically integrate object ID, will be discarded if shader doesn't use it + robj[:objectid] = robj.id + return robj +end + +include("GLRenderObject.jl") + +#################################################################################### +# freeing +function free(x) + try + unsafe_free(x) + catch e + isa(e, ContextNotAvailable) && return # if context got destroyed no need to worry! + rethrow(e) + end +end + +# OpenGL has the annoying habit of reusing id's when creating a new context +# We need to make sure to only free the current one +function unsafe_free(x::GLProgram) + is_context_active(x.context) || return + glDeleteProgram(x.id) + return +end + +function unsafe_free(x::GLBuffer) + # don't free from other context + is_context_active(x.context) || return + id = Ref(x.id) + glDeleteBuffers(1, id) + return +end + +function unsafe_free(x::Texture) + is_context_active(x.context) || return + id = Ref(x.id) + glDeleteTextures(x.id) + return +end + +function unsafe_free(x::GLVertexArray) + is_context_active(x.context) || return + id = Ref(x.id) + glDeleteVertexArrays(1, id) + return +end diff --git a/GLMakie/src/GLAbstraction/GLUniforms.jl b/GLMakie/src/GLAbstraction/GLUniforms.jl new file mode 100644 index 00000000000..efb4ef96ef0 --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLUniforms.jl @@ -0,0 +1,266 @@ +# Uniforms are OpenGL variables that stay the same for the entirety of a drawcall. +# There are a lot of functions, to upload them, as OpenGL doesn't rely on multiple dispatch. +# here is my approach, to handle all of the uniforms with one function, namely gluniform +# For uniforms, the Vector and Matrix types from ImmutableArrays should be used, as they map the relation almost 1:1 + +const GLSL_COMPATIBLE_NUMBER_TYPES = (GLfloat, GLint, GLuint, GLdouble) +const NATIVE_TYPES = Union{ + StaticArray, GLSL_COMPATIBLE_NUMBER_TYPES..., + ZeroIndex{GLint}, ZeroIndex{GLuint}, + GLBuffer, GPUArray, Shader, GLProgram, NativeMesh +} + +opengl_prefix(T) = error("Object $T is not a supported uniform element type") +opengl_postfix(T) = error("Object $T is not a supported uniform element type") + + +opengl_prefix(x::Type{T}) where {T <: Union{FixedPoint, Float32, Float16}} = "" +opengl_prefix(x::Type{T}) where {T <: Float64} = "d" +opengl_prefix(x::Type{Cint}) = "i" +opengl_prefix(x::Type{T}) where {T <: Union{Cuint, UInt8, UInt16}} = "u" + +opengl_postfix(x::Type{Float64}) = "dv" +opengl_postfix(x::Type{Float32}) = "fv" +opengl_postfix(x::Type{Cint}) = "iv" +opengl_postfix(x::Type{Cuint}) = "uiv" + + +function uniformfunc(typ::DataType, dims::Tuple{Int}) + Symbol(string("glUniform", first(dims), opengl_postfix(typ))) +end +function uniformfunc(typ::DataType, dims::Tuple{Int, Int}) + M, N = dims + Symbol(string("glUniformMatrix", M == N ? "$M" : "$(M)x$(N)", opengl_postfix(typ))) +end + +function gluniform(location::Integer, x::FSA) where FSA <: Union{StaticArray, Colorant} + xref = [x] + gluniform(location, xref) +end + +_size(p) = size(p) +_size(p::Colorant) = (length(p),) +_size(p::Type{T}) where {T <: Colorant} = (length(p),) +_ndims(p) = ndims(p) +_ndims(p::Type{T}) where {T <: Colorant} = 1 + +@generated function gluniform(location::Integer, x::Vector{FSA}) where FSA <: Union{StaticArray, Colorant} + func = uniformfunc(eltype(FSA), _size(FSA)) + callexpr = if _ndims(FSA) == 2 + :($func(location, length(x), GL_FALSE, x)) + else + :($func(location, length(x), x)) + end + quote + $callexpr + end +end + + +#Some additional uniform functions, not related to Imutable Arrays +gluniform(location::Integer, target::Integer, t::Texture) = gluniform(GLint(location), GLint(target), t) +gluniform(location::Integer, target::Integer, t::GPUVector) = gluniform(GLint(location), GLint(target), t.buffer) +gluniform(location::Integer, target::Integer, t::Node) = gluniform(GLint(location), GLint(target), to_value(t)) +gluniform(location::Integer, target::Integer, t::TextureBuffer) = gluniform(GLint(location), GLint(target), t.texture) +function gluniform(location::GLint, target::GLint, t::Texture) + activeTarget = GL_TEXTURE0 + UInt32(target) + glActiveTexture(activeTarget) + glBindTexture(t.texturetype, t.id) + gluniform(location, target) +end +gluniform(location::Integer, x::Enum) = gluniform(GLint(location), GLint(x)) + +function gluniform(loc::Integer, x::Node{T}) where T + gluniform(GLint(loc), to_value(x)) +end + +gluniform(location::Integer, x::Union{GLubyte, GLushort, GLuint}) = glUniform1ui(GLint(location), x) +gluniform(location::Integer, x::Union{GLbyte, GLshort, GLint, Bool}) = glUniform1i(GLint(location), x) +gluniform(location::Integer, x::GLfloat) = glUniform1f(GLint(location), x) +gluniform(location::Integer, x::GLdouble) = glUniform1d(GLint(location), x) + +#Uniform upload functions for julia arrays... +gluniform(location::GLint, x::Vector{Float32}) = glUniform1fv(location, length(x), x) +gluniform(location::GLint, x::Vector{GLdouble}) = glUniform1dv(location, length(x), x) +gluniform(location::GLint, x::Vector{GLint}) = glUniform1iv(location, length(x), x) +gluniform(location::GLint, x::Vector{GLuint}) = glUniform1uiv(location, length(x), x) + +glsl_typename(x::T) where {T} = glsl_typename(T) +glsl_typename(t::DataType) = error("Datatype $(t) not supported") +glsl_typename(t::Type{Nothing}) = "Nothing" +glsl_typename(t::Type{GLfloat}) = "float" +glsl_typename(t::Type{GLdouble}) = "double" +glsl_typename(t::Type{GLuint}) = "uint" +glsl_typename(t::Type{GLint}) = "int" +glsl_typename(t::Type{T}) where {T <: Union{StaticVector, Colorant}} = string(opengl_prefix(eltype(T)), "vec", length(T)) +glsl_typename(t::Type{TextureBuffer{T}}) where {T} = string(opengl_prefix(eltype(T)), "samplerBuffer") + +function glsl_typename(t::Texture{T, D}) where {T, D} + str = string(opengl_prefix(eltype(T)), "sampler", D, "D") + t.texturetype == GL_TEXTURE_2D_ARRAY && (str *= "Array") + str +end + +function glsl_typename(t::Type{T}) where T <: SMatrix + M, N = size(t) + string(opengl_prefix(eltype(t)), "mat", M==N ? M : string(M, "x", N)) +end +toglsltype_string(t::Node) = toglsltype_string(to_value(t)) +toglsltype_string(x::T) where {T<:Union{Real, StaticArray, Texture, Colorant, TextureBuffer, Nothing}} = "uniform $(glsl_typename(x))" +#Handle GLSL structs, which need to be addressed via single fields +function toglsltype_string(x::T) where T + if isa_gl_struct(x) + string("uniform ", T.name.name) + else + error("can't splice $T into an OpenGL shader. Make sure all fields are of a concrete type and isbits(FieldType)-->true") + end +end +toglsltype_string(t::Union{GLBuffer{T}, GPUVector{T}}) where {T} = string("in ", glsl_typename(T)) +# Gets used to access a +function glsl_variable_access(keystring, t::Texture{T, D}) where {T,D} + fields = SubString("rgba", 1, length(T)) + if t.texturetype == GL_TEXTURE_BUFFER + return string("texelFetch(", keystring, "index).", fields, ";") + end + return string("getindex(", keystring, "index).", fields, ";") +end +function glsl_variable_access(keystring, ::Union{Real, GLBuffer, GPUVector, StaticArray, Colorant}) + string(keystring, ";") +end +function glsl_variable_access(keystring, s::Node) + glsl_variable_access(keystring, to_value(s)) +end +function glsl_variable_access(keystring, t::Any) + error("no glsl variable calculation available for : ", keystring, " of type ", typeof(t)) +end + +function uniform_name_type(program::GLuint) + uniformLength = glGetProgramiv(program, GL_ACTIVE_UNIFORMS) + Dict{Symbol, GLenum}(ntuple(uniformLength) do i # take size and name + name, typ = glGetActiveUniform(program, i-1) + end) +end +function attribute_name_type(program::GLuint) + uniformLength = glGetProgramiv(program, GL_ACTIVE_ATTRIBUTES) + Dict{Symbol, GLenum}(ntuple(uniformLength) do i + name, typ = glGetActiveAttrib(program, i-1) + end) +end +function istexturesampler(typ::GLenum) + return ( + typ == GL_SAMPLER_BUFFER || typ == GL_INT_SAMPLER_BUFFER || typ == GL_UNSIGNED_INT_SAMPLER_BUFFER || + typ == GL_IMAGE_2D || + typ == GL_SAMPLER_1D || typ == GL_SAMPLER_2D || typ == GL_SAMPLER_3D || + typ == GL_UNSIGNED_INT_SAMPLER_1D || typ == GL_UNSIGNED_INT_SAMPLER_2D || typ == GL_UNSIGNED_INT_SAMPLER_3D || + typ == GL_INT_SAMPLER_1D || typ == GL_INT_SAMPLER_2D || typ == GL_INT_SAMPLER_3D || + typ == GL_SAMPLER_1D_ARRAY || typ == GL_SAMPLER_2D_ARRAY || + typ == GL_UNSIGNED_INT_SAMPLER_1D_ARRAY || typ == GL_UNSIGNED_INT_SAMPLER_2D_ARRAY || + typ == GL_INT_SAMPLER_1D_ARRAY || typ == GL_INT_SAMPLER_2D_ARRAY + ) +end + + +gl_promote(x::Type{T}) where {T <: Integer} = Cint +gl_promote(x::Type{Union{Int16, Int8}}) = x + +gl_promote(x::Type{T}) where {T <: Unsigned} = Cuint +gl_promote(x::Type{Union{UInt16, UInt8}}) = x + +gl_promote(x::Type{T}) where {T <: AbstractFloat} = Float32 +gl_promote(x::Type{Float16}) = x + +gl_promote(x::Type{T}) where {T <: Normed} = N0f32 +gl_promote(x::Type{N0f16}) = x +gl_promote(x::Type{N0f8}) = x + +const Color3{T} = Colorant{T, 3} +const Color4{T} = Colorant{T, 4} + +gl_promote(x::Type{Bool}) = GLboolean +gl_promote(x::Type{T}) where {T <: Gray} = Gray{gl_promote(eltype(T))} +gl_promote(x::Type{T}) where {T <: Color3} = RGB{gl_promote(eltype(T))} +gl_promote(x::Type{T}) where {T <: Color4} = RGBA{gl_promote(eltype(T))} +gl_promote(x::Type{T}) where {T <: BGRA} = BGRA{gl_promote(eltype(T))} +gl_promote(x::Type{T}) where {T <: BGR} = BGR{gl_promote(eltype(T))} + + +gl_promote(x::Type{T}) where {T <: StaticVector} = similar_type(T, gl_promote(eltype(T))) + +gl_promote(x::Type{T}) where {T <: GeometryBasics.Mesh} = NativeMesh{T} + +gl_convert(x::AbstractVector{Vec3f0}) = x + +gl_convert(x::T) where {T <: Number} = gl_promote(T)(x) +gl_convert(x::T) where {T <: Colorant} = gl_promote(T)(x) +gl_convert(x::T) where {T <: AbstractMesh} = gl_convert(x) +gl_convert(x::T) where {T <: GeometryBasics.Mesh} = gl_promote(T)(x) +gl_convert(x::Node{T}) where {T <: GeometryBasics.Mesh} = gl_promote(T)(x) + +gl_convert(s::Vector{Matrix{T}}) where {T<:Colorant} = Texture(s) +gl_convert(s::Nothing) = s + + +isa_gl_struct(x::AbstractArray) = false +isa_gl_struct(x::NATIVE_TYPES) = false +isa_gl_struct(x::Colorant) = false +function isa_gl_struct(x::T) where T + !isconcretetype(T) && return false + if T <: Tuple + return false + end + fnames = fieldnames(T) + !isempty(fnames) && all(name -> isconcretetype(fieldtype(T, name)) && isbits(getfield(x, name)), fnames) +end +function gl_convert_struct(x::T, uniform_name::Symbol) where T + if isa_gl_struct(x) + return Dict{Symbol, Any}(map(fieldnames(x)) do name + (Symbol("$uniform_name.$name") => gl_convert(getfield(x, name))) + end) + else + error("can't convert $x to a OpenGL type. Make sure all fields are of a concrete type and isbits(FieldType)-->true") + end +end + + +# native types don't need convert! +gl_convert(a::T) where {T <: NATIVE_TYPES} = a +gl_convert(s::Node{T}) where {T <: NATIVE_TYPES} = s +gl_convert(s::Node{T}) where T = const_lift(gl_convert, s) +gl_convert(x::StaticVector{N, T}) where {N, T} = map(gl_promote(T), x) +gl_convert(x::SMatrix{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) + +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...) +end + +gl_convert(::Type{<: GLBuffer}, x::GLBuffer; kw_args...) = x +gl_convert(::Type{Texture}, x::Texture) = x +gl_convert(::Type{<: GPUArray}, x::GPUArray) = x + +function gl_convert(::Type{T}, a::Vector{Array{X, 2}}; kw_args...) where {T <: Texture, X} + T(a; kw_args...) +end +gl_convert(::Type{<: GPUArray}, a::Node{<: StaticVector}) = gl_convert(a) + +function gl_convert(::Type{T}, a::Node{<: AbstractArray{X, N}}; kw_args...) where {T <: GPUArray, X, N} + TGL = gl_promote(X) + s = (X == TGL) ? a : lift(x-> convert(Array{TGL, N}, x), a) + 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/GLAbstraction/GLUtils.jl b/GLMakie/src/GLAbstraction/GLUtils.jl new file mode 100644 index 00000000000..de3f8bba6c9 --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLUtils.jl @@ -0,0 +1,239 @@ +function print_with_lines(out::IO, text::AbstractString) + io = IOBuffer() + for (i,line) in enumerate(split(text, "\n")) + println(io, @sprintf("%-4d: %s", i, line)) + end + write(out, take!(io)) +end +print_with_lines(text::AbstractString) = print_with_lines(stdout, text) + +""" +Style Type, which is used to choose different visualization/editing styles via multiple dispatch +Usage pattern: +visualize(::Style{:Default}, ...) = do something +visualize(::Style{:MyAwesomeNewStyle}, ...) = do something different +""" +struct Style{StyleValue} +end +Style(x::Symbol) = Style{x}() +Style() = Style{:Default}() +mergedefault!(style::Style{S}, styles, customdata) where {S} = merge!(copy(styles[S]), Dict{Symbol, Any}(customdata)) +macro style_str(string) + Style{Symbol(string)} +end +export @style_str + +""" +splats keys from a dict into variables +""" +macro materialize(dict_splat) + keynames, dict = dict_splat.args + keynames = isa(keynames, Symbol) ? [keynames] : keynames.args + dict_instance = gensym() + kd = [:($key = $dict_instance[$(Expr(:quote, key))]) for key in keynames] + kdblock = Expr(:block, kd...) + expr = quote + $dict_instance = $dict # handle if dict is not a variable but an expression + $kdblock + end + esc(expr) +end + +""" +splats keys from a dict into variables and removes them +""" +macro materialize!(dict_splat) + keynames, dict = dict_splat.args + keynames = isa(keynames, Symbol) ? [keynames] : keynames.args + dict_instance = gensym() + kd = [:($key = pop!($dict_instance, $(Expr(:quote, key)))) for key in keynames] + kdblock = Expr(:block, kd...) + expr = quote + $dict_instance = $dict # handle if dict is not a variable but an expression + $kdblock + end + esc(expr) +end + +""" +Needed to match the lazy gl_convert exceptions. + `Target`: targeted OpenGL type + `x`: the variable that gets matched +""" +matches_target(::Type{Target}, x::T) where {Target, T} = applicable(gl_convert, Target, x) || T <: Target # it can be either converted to Target, or it's already the target +matches_target(::Type{Target}, x::Node{T}) where {Target, T} = applicable(gl_convert, Target, x) || T <: Target +matches_target(::Function, x) = true +matches_target(::Function, x::Nothing) = false + +signal_convert(T1, y::T2) where {T2<:Node} = lift(convert, Node(T1), y) + + +""" +Takes a dict and inserts defaults, if not already available. +The variables are made accessible in local scope, so things like this are possible: +gen_defaults! dict begin + a = 55 + b = a * 2 # variables, like a, will get made visible in local scope + c::JuliaType = X # `c` needs to be of type JuliaType. `c` will be made available with it's original type and then converted to JuliaType when inserted into `dict` + d = x => GLType # OpenGL convert target. Get's only applied if `x` is convertible to GLType. Will only be converted when passed to RenderObject + d = x => \"doc string\" + d = x => (GLType, \"doc string and gl target\") +end +""" +macro gen_defaults!(dict, args) + args.head == :block || error("second argument needs to be a block of form + begin + a = 55 + b = a * 2 # variables, like a, will get made visible in local scope + c::JuliaType = X # c needs to be of type JuliaType. c will be made available with it's original type and then converted to JuliaType when inserted into data + d = x => GLType # OpenGL convert target. Get's only applied if x is convertible to GLType. Will only be converted when passed to RenderObject + end") + tuple_list = args.args + dictsym = gensym() + return_expression = Expr(:block) + push!(return_expression.args, :($dictsym = $dict)) # dict could also be an expression, so we need to asign it to a variable at the beginning + push!(return_expression.args, :(gl_convert_targets = get!($dictsym, :gl_convert_targets, Dict{Symbol, Any}()))) # exceptions for glconvert. + push!(return_expression.args, :(doc_strings = get!($dictsym, :doc_string, Dict{Symbol, Any}()))) # exceptions for glconvert. + # @gen_defaults can be used multiple times, so we need to reuse gl_convert_targets if already in here + for (i, elem) in enumerate(tuple_list) + opengl_convert_target = :() # is optional, so first is an empty expression + convert_target = :() # is optional, so first is an empty expression + doc_strings = :() + if Meta.isexpr(elem, :(=)) + key_name, value_expr = elem.args + if isa(key_name, Expr) && key_name.head == :(::) # we need to convert to a julia type + key_name, convert_target = key_name.args + convert_target = :(GLAbstraction.signal_convert($convert_target, $key_name)) + else + convert_target = :($key_name) + end + key_sym = Expr(:quote, key_name) + if isa(value_expr, Expr) && value_expr.head == :call && value_expr.args[1] == :(=>) # we might need to insert a convert target + value_expr, target = value_expr.args[2:end] + undecided = [] + if isa(target, Expr) + undecided = target.args + else + push!(undecided, target) + end + for elem in undecided + isa(elem, Expr) && continue # + if isa(elem, AbstractString) # only docstring + doc_strings = :(doc_strings[$key_sym] = $elem) + elseif isa(elem, Symbol) + opengl_convert_target = quote + if GLAbstraction.matches_target($elem, $key_name) + gl_convert_targets[$key_sym] = $elem + end + end + end + end + end + expr = quote + $key_name = if haskey($dictsym, $key_sym) + $dictsym[$key_sym] + else + $value_expr # in case that evaluating value_expr is expensive, we use a branch instead of get(dict, key, default) + end + $dictsym[$key_sym] = $convert_target + $opengl_convert_target + $doc_strings + end + push!(return_expression.args, expr) + end + end + #push!(return_expression.args, :($dictsym[:gl_convert_targets] = gl_convert_targets)) #just pass the targets via the dict + push!(return_expression.args, :($dictsym)) #return dict + esc(return_expression) +end +export @gen_defaults! + +makesignal(s::Node) = s +makesignal(v) = Node(v) + +@inline const_lift(f::Union{DataType, Type, Function}, inputs...) = lift(f, map(makesignal, inputs)...) +export const_lift + +isnotempty(x) = !isempty(x) +AND(a,b) = a&&b +OR(a,b) = a||b + +#Meshtype holding native OpenGL data. +struct NativeMesh{MeshType <: GeometryBasics.Mesh} + data::Dict{Symbol, Any} +end + +export NativeMesh + +NativeMesh(m::T) where {T <: GeometryBasics.Mesh} = NativeMesh{T}(m) +NativeMesh(m::Observable{T}) where {T <: GeometryBasics.Mesh} = NativeMesh{T}(m) + +convert_texcoordinates(uv::AbstractVector{Vec2f0}) = uv +convert_texcoordinates(x::AbstractVector{<:Number}) = convert(Vector{Float32}, x) + +function NativeMesh{T}(mesh::T) where T <: GeometryBasics.Mesh + result = Dict{Symbol, Any}() + attribs = GeometryBasics.attributes(mesh) + result[:faces] = indexbuffer(faces(mesh)) + if isempty(attribs) + # TODO position only shows up as attribute + # when it has meta informtion + result[:vertices] = GLBuffer(collect(metafree(coordinates(mesh)))) + end + for (field, val) in attribs + if val isa Makie.Sampler + result[:image] = Texture(val.colors) + result[:texturecoordinates] = GLBuffer(convert_texcoordinates(val.values)) + if val.scaling.range !== nothing + result[:color_norm] = Observable(Vec2f0(val.scaling.range)) + end + elseif field in (:position, :uv, :uvw, :normals, :attribute_id, :color) + if field == :color + field = :vertex_color + elseif field in (:uv, :uvw) + field = :texturecoordinates + elseif field == :position + field = :vertices + end + if val isa AbstractVector + result[field] = GLBuffer(metafree(val)) + end + else + result[field] = Texture(val) + end + end + return NativeMesh{T}(result) +end + + +function NativeMesh{T}(m::Node{T}) where T <: GeometryBasics.Mesh + result = NativeMesh{T}(m[]) + on(m) do mesh + + attribs = GeometryBasics.attributes(mesh) + + update!(result.data[:faces], faces(mesh)) + + if isempty(attribs) + # TODO position only shows up as attribute + # when it has meta informtion + update!(result.data[:vertices], metafree(coordinates(mesh))) + end + + for (field, val) in attribs + if val isa Makie.Sampler + update!(result.data[:image], val.colors) + update!(result.data[:texturecoordinates], convert_texcoordinates(val.values)) + if val.scaling.range !== nothing + result.data[:color_norm][] = Vec2f0(val.scaling.range) + end + else + field == :color && (field = :vertex_color) + field == :uv && (field = :texturecoordinates) + field == :position && (field = :vertices) + haskey(result.data, field) && update!(result.data[field], val) + end + end + end + return result +end diff --git a/GLMakie/src/GLAbstraction/precompile.jl b/GLMakie/src/GLAbstraction/precompile.jl new file mode 100644 index 00000000000..da3c7c61eb9 --- /dev/null +++ b/GLMakie/src/GLAbstraction/precompile.jl @@ -0,0 +1,107 @@ +const __bodyfunction__ = Dict{Method,Any}() + +# Find keyword "body functions" (the function that contains the body +# as written by the developer, called after all missing keyword-arguments +# have been assigned values), in a manner that doesn't depend on +# gensymmed names. +# `mnokw` is the method that gets called when you invoke it without +# supplying any keywords. +function __lookup_kwbody__(mnokw::Method) + function getsym(arg) + isa(arg, Symbol) && return arg + @assert isa(arg, GlobalRef) + return arg.name + end + + f = get(__bodyfunction__, mnokw, nothing) + if f === nothing + fmod = mnokw.module + # The lowered code for `mnokw` should look like + # %1 = mkw(kwvalues..., #self#, args...) + # return %1 + # where `mkw` is the name of the "active" keyword body-function. + ast = Base.uncompressed_ast(mnokw) + if isa(ast, Core.CodeInfo) && length(ast.code) >= 2 + callexpr = ast.code[end-1] + if isa(callexpr, Expr) && callexpr.head == :call + fsym = callexpr.args[1] + if isa(fsym, Symbol) + f = getfield(fmod, fsym) + elseif isa(fsym, GlobalRef) + if fsym.mod === Core && fsym.name === :_apply + f = getfield(mnokw.module, getsym(callexpr.args[2])) + elseif fsym.mod === Core && fsym.name === :_apply_iterate + f = getfield(mnokw.module, getsym(callexpr.args[3])) + else + f = getfield(fsym.mod, fsym.name) + end + else + f = missing + end + else + f = missing + end + else + f = missing + end + __bodyfunction__[mnokw] = f + end + return f +end + +function _precompile_() + ccall(:jl_generating_output, Cint, ()) == 1 || return nothing + Base.precompile(Tuple{typeof(get_template!),String,Dict{String, String},Dict{Symbol, Any}}) # time: 0.21933882 + isdefined(GLAbstraction, Symbol("#63#68")) && Base.precompile(Tuple{getfield(GLAbstraction, Symbol("#63#68"))}) # time: 0.086304404 + Base.precompile(Tuple{typeof(gluniform),Int32,Observable{Vec{2, Float32}}}) # time: 0.06477806 + Base.precompile(Tuple{Core.kwftype(typeof(Type)),NamedTuple{(:x_repeat,), Tuple{Symbol}},Type{Texture},Vector{Float16}}) # time: 0.03858548 + Base.precompile(Tuple{typeof(signal_convert),Type,Observable{Vec2{Int32}}}) # time: 0.038141333 + Base.precompile(Tuple{typeof(gl_convert),Type{GLBuffer},Observable{Vec{2, Float32}}}) # time: 0.031721786 + Base.precompile(Tuple{typeof(toglsltype_string),GLBuffer{Point{3, Float32}}}) # time: 0.03062245 + Base.precompile(Tuple{typeof(signal_convert),Type,Observable{Float32}}) # time: 0.022515636 + Base.precompile(Tuple{typeof(compile_program),Vector{Shader},Vector{Tuple{Int64, String}}}) # time: 0.021999607 + let fbody = try __lookup_kwbody__(which(TextureParameters, (Type,Int64,))) catch missing end + if !ismissing(fbody) + precompile(fbody, (Symbol,Symbol,Symbol,Symbol,Symbol,Float32,Type{TextureParameters},Type,Int64,)) + end + end # time: 0.019532876 + Base.precompile(Tuple{typeof(gl_convert),Observable{Any}}) # time: 0.01664884 + Base.precompile(Tuple{typeof(gl_convert),Observable{Bool}}) # time: 0.0149467 + Base.precompile(Tuple{typeof(gl_convert),Observable{RGBA{Float32}}}) # time: 0.014625993 + Base.precompile(Tuple{typeof(const_lift),typeof(length),Observable{Vector{Point{2, Float32}}}}) # time: 0.014371832 + Base.precompile(Tuple{typeof(gl_convert),Type{GLBuffer},Observable{Vector{Point{3, Float32}}}}) # time: 0.01388594 + Base.precompile(Tuple{typeof(gl_convert),Observable{Int64}}) # time: 0.01345424 + Base.precompile(Tuple{typeof(const_lift),typeof(length),Observable{Vector{Point{3, Float32}}}}) # time: 0.011757969 + Base.precompile(Tuple{typeof(gl_convert),Type{GLBuffer},Observable{Vector{Vec{2, Float32}}}}) # time: 0.011719398 + isdefined(GLAbstraction, Symbol("#LazyShader#49#50")) && Base.precompile(Tuple{getfield(GLAbstraction, Symbol("#LazyShader#49#50")),Base.Iterators.Pairs{Symbol, Dict{String, String}, Tuple{Symbol}, NamedTuple{(:view,), Tuple{Dict{String, String}}}},Type{LazyShader},String,Vararg{String, N} where N}) # time: 0.010064982 + Base.precompile(Tuple{typeof(gl_convert),Type{GLBuffer},Observable{Vector{Vec{4, Float32}}}}) # time: 0.008807838 + isdefined(GLAbstraction, Symbol("#63#68")) && Base.precompile(Tuple{getfield(GLAbstraction, Symbol("#63#68"))}) # time: 0.008238689 + Base.precompile(Tuple{typeof(gl_convert),Type{GLBuffer},Observable{Vector{Point{2, Float32}}}}) # time: 0.008220657 + Base.precompile(Tuple{typeof(gl_convert),Type{GLBuffer},Observable{Vector{RGBA{Float32}}}}) # time: 0.007779626 + Base.precompile(Tuple{typeof(toglsltype_string),GLBuffer{Vec{4, Float32}}}) # time: 0.007245759 + Base.precompile(Tuple{typeof(isa_gl_struct),Observable{Int64}}) # time: 0.00689424 + Base.precompile(Tuple{typeof(toglsltype_string),Texture{Float16, 2}}) # time: 0.006558549 + Base.precompile(Tuple{typeof(gl_convert),Type{GLBuffer},Observable{Vector{Float32}}}) # time: 0.006534443 + Base.precompile(Tuple{typeof(gl_convert),SMatrix{4, 4, Float32, 16}}) # time: 0.0058618 + Base.precompile(Tuple{typeof(isa_gl_struct),Observable{Float32}}) # time: 0.005198346 + Base.precompile(Tuple{typeof(isa_gl_struct),Observable{Any}}) # time: 0.004741108 + Base.precompile(Tuple{typeof(toglsltype_string),Texture{Float16, 1}}) # time: 0.004597425 + Base.precompile(Tuple{typeof(toglsltype_string),GLBuffer{RGBA{Float32}}}) # time: 0.00437181 + Base.precompile(Tuple{typeof(isa_gl_struct),Observable{Vec2{Int32}}}) # time: 0.004354946 + Base.precompile(Tuple{typeof(isa_gl_struct),Observable{Int32}}) # time: 0.004301191 + Base.precompile(Tuple{typeof(isa_gl_struct),Observable{SMatrix{4, 4, Float32, 16}}}) # time: 0.004184706 + Base.precompile(Tuple{typeof(isa_gl_struct),Observable{Vec{3, Float32}}}) # time: 0.004105391 + Base.precompile(Tuple{typeof(isa_gl_struct),Observable{Vec{2, Float32}}}) # time: 0.004101697 + Base.precompile(Tuple{typeof(toglsltype_string),GLBuffer{Point{2, Float32}}}) # time: 0.003962705 + Base.precompile(Tuple{typeof(toglsltype_string),GLBuffer{Vec{2, Float32}}}) # time: 0.003796558 + Base.precompile(Tuple{typeof(isa_gl_struct),Observable{RGBA{Float32}}}) # time: 0.003778398 + Base.precompile(Tuple{typeof(isa_gl_struct),Observable{Bool}}) # time: 0.00368915 + Base.precompile(Tuple{typeof(isa_gl_struct),Dict{Symbol, Any}}) # time: 0.003686279 + Base.precompile(Tuple{typeof(isa_gl_struct),Nothing}) # time: 0.003181062 + Base.precompile(Tuple{typeof(isa_gl_struct),Symbol}) # time: 0.003076398 + Base.precompile(Tuple{typeof(isa_gl_struct),Bool}) # time: 0.003013663 + Base.precompile(Tuple{typeof(gl_convert),Vec{2, Float32}}) # time: 0.002978386 + Base.precompile(Tuple{typeof(mustache2replacement),String,Dict{String, String},Dict{Symbol, Any}}) # time: 0.002830558 + Base.precompile(Tuple{typeof(gluniform),Int32,Int64,Texture{RGBA{N0f8}, 2}}) # time: 0.002434047 + Base.precompile(Tuple{typeof(map_texture_paramers),Tuple{Symbol, Symbol}}) # time: 0.00121505 +end diff --git a/GLMakie/src/GLAbstraction/shaderabstraction.jl b/GLMakie/src/GLAbstraction/shaderabstraction.jl new file mode 100644 index 00000000000..e88c2e77e7d --- /dev/null +++ b/GLMakie/src/GLAbstraction/shaderabstraction.jl @@ -0,0 +1,23 @@ + +const GLContext = Any +# struct GLContext{T} <: ShaderAbstractions.AbstractContext +# context::T +# opengl_version::VersionNumber +# glsl_version::VersionNumber +# # Use a unique id, since we can't track this via pointer identity +# # (OpenGL may reuse the same pointers) +# unique_id::UInt64 +# end +# +# let counter = Threads.Atomic{UInt64}(0) +# global unique_context_counter +# function unique_context_counter() +# # dont start at zero, so we can keep zero special +# counter[] = counter[] + 1 +# return counter[] +# end +# end +# +# function GLContext(::Nothing) +# return GLContext(nothing, v"0.0.0", v"0.0.0", UInt64(0)) +# end diff --git a/GLMakie/src/GLMakie.jl b/GLMakie/src/GLMakie.jl new file mode 100644 index 00000000000..5c094a3ffd7 --- /dev/null +++ b/GLMakie/src/GLMakie.jl @@ -0,0 +1,54 @@ +module GLMakie + +using ModernGL, FixedPointNumbers, Colors, GeometryBasics, StaticArrays +using Makie, FileIO + +using Makie: @key_str, Key, broadcast_foreach, to_ndim, NativeFont +using Makie: Scene, Lines, Text, Image, Heatmap, Scatter +using Makie: convert_attribute, @extractvalue, LineSegments +using Makie: @get_attribute, to_value, to_colormap, extrema_nan +using Makie: ClosedInterval, (..) +using Makie: inline! +using ShaderAbstractions +using FreeTypeAbstraction + +using Base: RefValue +import Base: push!, isopen, show +using Base.Iterators: repeated, drop + +using LinearAlgebra + +for name in names(Makie) + @eval import Makie: $(name) + @eval export $(name) +end +export inline! + +struct GLBackend <: Makie.AbstractBackend +end + +loadshader(name) = normpath(joinpath(@__DIR__, "..", "assets", "shader", name)) + +# don't put this into try catch, to not mess with normal errors +include("gl_backend.jl") + +function activate!(use_display=true) + b = GLBackend() + Makie.register_backend!(b) + Makie.set_glyph_resolution!(Makie.High) + Makie.current_backend[] = b + Makie.inline!(!use_display) +end + +function __init__() + activate!() +end + +export set_window_config! + +if Base.VERSION >= v"1.4.2" + include("precompile.jl") + _precompile_() +end + +end diff --git a/GLMakie/src/GLVisualize/GLVisualize.jl b/GLMakie/src/GLVisualize/GLVisualize.jl new file mode 100644 index 00000000000..7124c821e5c --- /dev/null +++ b/GLMakie/src/GLVisualize/GLVisualize.jl @@ -0,0 +1,42 @@ +module GLVisualize + +using ..GLAbstraction +using Makie: RaymarchAlgorithm, IsoValue, Absorption, MaximumIntensityProjection, AbsorptionRGBA, IndexedAbsorptionRGBA + +using ..GLMakie.GLFW +using ModernGL +using StaticArrays +using GeometryBasics +using Colors +using Makie +using FixedPointNumbers +using FileIO +using Markdown +using Observables + +import Base: merge, convert, show +using Base.Iterators: Repeated, repeated +using LinearAlgebra + +import Makie: to_font, glyph_uv_width! +import ..GLMakie: get_texture!, loadshader + +const GLBoundingBox = FRect3D + +include("visualize_interface.jl") +export visualize # Visualize an object + +include(joinpath("visualize", "lines.jl")) +include(joinpath("visualize", "image_like.jl")) +include(joinpath("visualize", "mesh.jl")) +include(joinpath("visualize", "particles.jl")) +include(joinpath("visualize", "surface.jl")) + +export CIRCLE, RECTANGLE, ROUNDED_RECTANGLE, DISTANCEFIELD, TRIANGLE + +if Base.VERSION >= v"1.4.2" + include("precompile.jl") + _precompile_() +end + +end # module diff --git a/GLMakie/src/GLVisualize/precompile.jl b/GLMakie/src/GLVisualize/precompile.jl new file mode 100644 index 00000000000..3c40a882059 --- /dev/null +++ b/GLMakie/src/GLVisualize/precompile.jl @@ -0,0 +1,20 @@ +function _precompile_() + ccall(:jl_generating_output, Cint, ()) == 1 || return nothing + Base.precompile(Tuple{typeof(line_visualization),Observable{Vector{Point{2, Float32}}},Dict{Symbol, Any}}) # time: 0.22955656 + Base.precompile(Tuple{typeof(assemble_robj),Dict{Symbol, Any},GLVisualizeShader,GeometryBasics.HyperRectangle{3, Float32},UInt32,Nothing,Nothing}) # time: 0.112225175 + Base.precompile(Tuple{typeof(to_index_buffer),Observable{Vector{Int64}}}) # time: 0.09670886 + Base.precompile(Tuple{typeof(ticks),Vector{T} where T,Int64}) # time: 0.07404702 + Base.precompile(Tuple{typeof(vec2quaternion),Observable{Vector{Quaternionf0}}}) # time: 0.043232486 + isdefined(GLVisualize, Symbol("#GLVisualizeShader#8#10")) && Base.precompile(Tuple{getfield(GLVisualize, Symbol("#GLVisualizeShader#8#10")),Dict{String, String},Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}},Type{GLVisualizeShader},String,Vararg{String, N} where N}) # time: 0.035577767 + Base.precompile(Tuple{typeof(_position_calc),Observable{Vector{Point{3, Float32}}},Type{GLBuffer}}) # time: 0.016643895 + Base.precompile(Tuple{typeof(_position_calc),Observable{Vector{Point{2, Float32}}},Type{GLBuffer}}) # time: 0.013070196 + Base.precompile(Tuple{typeof(vec2quaternion),Observable{Quaternionf0}}) # time: 0.012976533 + Base.precompile(Tuple{Core.kwftype(typeof(Type)),NamedTuple{(:view,), Tuple{Dict{String, String}}},Type{GLVisualizeShader},String,String,String,String,String}) # time: 0.009165488 + Base.precompile(Tuple{typeof(visualize),Any,Any,Any}) # time: 0.00876585 + Base.precompile(Tuple{typeof(ticks),Vector{Float64},Int64}) # time: 0.008059256 + Base.precompile(Tuple{typeof(position_calc),Observable{Vector{Point{3, Float32}}},Vararg{Any, N} where N}) # time: 0.00481658 + Base.precompile(Tuple{typeof(assemble_shader),Any}) # time: 0.004637692 + Base.precompile(Tuple{typeof(position_calc),Observable{Vector{Point{2, Float32}}},Vararg{Any, N} where N}) # time: 0.004362621 + Base.precompile(Tuple{typeof(primitive_uv_offset_width),Circle{Float32}}) # time: 0.0011835 + Base.precompile(Tuple{typeof(primitive_offset),Observable{Circle{Float32}},Any}) # time: 0.001028085 +end diff --git a/GLMakie/src/GLVisualize/visualize/image_like.jl b/GLMakie/src/GLVisualize/visualize/image_like.jl new file mode 100644 index 00000000000..38e4238d83d --- /dev/null +++ b/GLMakie/src/GLVisualize/visualize/image_like.jl @@ -0,0 +1,121 @@ +""" +A matrix of colors is interpreted as an image +""" +_default(::Node{Array{RGBA{N0f8}, 2}}, ::Style{:default}, ::Dict{Symbol,Any}) + + +function _default(main::MatTypes{T}, ::Style, data::Dict) where T <: Colorant + @gen_defaults! data begin + spatialorder = "yx" + end + if !(spatialorder in ("xy", "yx")) + error("Spatial order only accepts \"xy\" or \"yz\" as a value. Found: $spatialorder") + end + ranges = get(data, :ranges) do + const_lift(main, spatialorder) do m, s + (0:size(m, s == "xy" ? 1 : 2), 0:size(m, s == "xy" ? 2 : 1)) + end + end + delete!(data, :ranges) + @gen_defaults! data begin + image = main => (Texture, "image, can be a Texture or Array of colors") + position_x = nothing => Texture + position_y = nothing => Texture + primitive = const_lift(ranges) do r + x, y = minimum(r[1]), minimum(r[2]) + xmax, ymax = maximum(r[1]), maximum(r[2]) + return FRect2D(x, y, xmax - x, ymax - y) + end => to_uvmesh + preferred_camera = :orthographic_pixel + fxaa = false + shader = GLVisualizeShader("fragment_output.frag", "image.vert", "texture.frag", + view = Dict("uv_swizzle" => "o_uv.$(spatialorder)")) + end +end + +function to_uvmesh(geom) + return NativeMesh(const_lift(GeometryBasics.uv_mesh, geom)) +end + +function to_plainmesh(geom) + return NativeMesh(const_lift(GeometryBasics.triangle_mesh, geom)) +end + +""" +A matrix of Intensities will result in a contourf kind of plot +""" +function gl_heatmap(main::MatTypes{T}, data::Dict) where T <: AbstractFloat + @gen_defaults! data begin + intensity = main => Texture + primitive = Rect2D(0f0,0f0,1f0,1f0) => native_triangle_mesh + nan_color = RGBAf0(1, 0, 0, 1) + highclip = RGBAf0(0, 0, 0, 0) + lowclip = RGBAf0(0, 0, 0, 0) + color_map = nothing => Texture + color_norm = nothing + stroke_width::Float32 = 0.0f0 + levels::Float32 = 0f0 + stroke_color = RGBA{Float32}(0,0,0,0) + shader = GLVisualizeShader("fragment_output.frag", "heatmap.vert", "intensity.frag") + fxaa = false + end + return data +end + + +#Volumes +const VolumeElTypes = Union{Gray, AbstractFloat} + +const default_style = Style{:default}() + +using .GLAbstraction: StandardPrerender + +struct VolumePrerender + sp::StandardPrerender +end +VolumePrerender(a, b) = VolumePrerender(StandardPrerender(a, b)) + +function (x::VolumePrerender)() + x.sp() + glEnable(GL_CULL_FACE) + glCullFace(GL_FRONT) +end + +function _default(main::VolumeTypes{T}, s::Style, data::Dict) where T <: VolumeElTypes + @gen_defaults! data begin + volumedata = main => Texture + hull = FRect3D(Vec3f0(0), Vec3f0(1)) => to_plainmesh + model = Mat4f0(I) + modelinv = const_lift(inv, model) + color_map = default(Vector{RGBA}, s) => Texture + color_norm = color_map == nothing ? nothing : const_lift(extrema2f0, main) + color = color_map == nothing ? default(RGBA, s) : nothing + + algorithm = MaximumIntensityProjection + absorption = 1f0 + isovalue = 0.5f0 + isorange = 0.01f0 + shader = GLVisualizeShader("fragment_output.frag", "util.vert", "volume.vert", "volume.frag") + prerender = VolumePrerender(data[:transparency], data[:overdraw]) + postrender = () -> glDisable(GL_CULL_FACE) + end + return data +end + +function _default(main::VolumeTypes{T}, s::Style, data::Dict) where T <: RGBA + @gen_defaults! data begin + volumedata = main => Texture + hull = FRect3D(Vec3f0(0), Vec3f0(1)) => to_plainmesh + model = Mat4f0(I) + modelinv = const_lift(inv, model) + # These don't do anything but are needed for type specification in the frag shader + color_map = nothing => Texture + color_norm = nothing + color = color_map === nothing ? default(RGBA, s) : nothing + + algorithm = AbsorptionRGBA + shader = GLVisualizeShader("fragment_output.frag", "util.vert", "volume.vert", "volume.frag") + prerender = VolumePrerender(data[:transparency], data[:overdraw]) + postrender = () -> glDisable(GL_CULL_FACE) + end +end diff --git a/GLMakie/src/GLVisualize/visualize/lines.jl b/GLMakie/src/GLVisualize/visualize/lines.jl new file mode 100644 index 00000000000..9b6fb8279e0 --- /dev/null +++ b/GLMakie/src/GLVisualize/visualize/lines.jl @@ -0,0 +1,131 @@ +function sumlengths(points) + T = eltype(points[1]) + result = zeros(T, length(points)) + for i=1:length(points) + i0 = max(i-1,1) + p1, p2 = points[i0], points[i] + if !(any(map(isnan, p1)) || any(map(isnan, p2))) + result[i] = result[i0] + norm(p1-p2) + else + result[i] = result[i0] + end + end + result +end + +intensity_convert(intensity, verts) = intensity +function intensity_convert(intensity::VecOrSignal{T}, verts) where T + if length(to_value(intensity)) == length(to_value(verts)) + GLBuffer(intensity) + else + Texture(intensity) + end +end +function intensity_convert_tex(intensity::VecOrSignal{T}, verts) where T + if length(to_value(intensity)) == length(to_value(verts)) + TextureBuffer(intensity) + else + Texture(intensity) + end +end +#TODO NaNMath.min/max? +dist(a, b) = abs(a-b) +mindist(x, a, b) = min(dist(a, x), dist(b, x)) +function gappy(x, ps) + n = length(ps) + x <= first(ps) && return first(ps) - x + for j=1:(n-1) + p0 = ps[j] + p1 = ps[min(j+1, n)] + if p0 <= x && p1 >= x + return mindist(x, p0, p1) * (isodd(j) ? 1 : -1) + end + end + return last(ps) - x +end +function ticks(points, resolution) + Float16[gappy(x, points) for x = range(first(points), stop=last(points), length=resolution)] +end + + +# ambigious signature +function _default(position::VectorTypes{<: Point}, s::style"lines", data::Dict) + line_visualization(position, data) +end +function _default(position::MatTypes{<:Point}, s::style"lines", data::Dict) + line_visualization(position, data) +end +function line_visualization(position::Union{VectorTypes{T}, MatTypes{T}}, data::Dict) where T<:Point + p_vec = if isa(position, GPUArray) + position + else + const_lift(vec, position) + end + + @gen_defaults! data begin + dims::Vec{2, Int32} = const_lift(position) do p + sz = ndims(p) == 1 ? (length(p), 1) : size(p) + Vec{2, Int32}(sz) + end + vertex = p_vec => GLBuffer + intensity = nothing + color_map = nothing => Texture + color_norm = nothing + color = (color_map == nothing ? default(RGBA, s) : nothing) => GLBuffer + thickness::Float32 = 2f0 + pattern = nothing + fxaa = false + preferred_camera = :orthographic_pixel + # Duplicate the vertex indices on the ends of the line, as our geometry + # shader in `layout(lines_adjacency)` mode requires each rendered + # segment to have neighbouring vertices. + indices = const_lift((p)-> isempty(p) ? Cuint[] : [1; 1:length(p); length(p)], p_vec) => to_index_buffer + shader = GLVisualizeShader("fragment_output.frag", "util.vert", "lines.vert", "lines.geom", "lines.frag") + gl_primitive = GL_LINE_STRIP_ADJACENCY + valid_vertex = const_lift(p_vec) do pv + map(p-> Float32(all(isfinite, p)), pv) + end => GLBuffer + end + if pattern != nothing + if !isa(pattern, Texture) + if !isa(pattern, Vector) + error("Pattern needs to be a Vector of floats. Found: $(typeof(pattern))") + end + tex = GLAbstraction.Texture(ticks(pattern, 100), x_repeat = :repeat) + data[:pattern] = tex + end + @gen_defaults! data begin + pattern_length = Float32(last(pattern)) + lastlen = const_lift(sumlengths, p_vec) => GLBuffer + maxlength = const_lift(last, lastlen) + end + end + data[:intensity] = intensity_convert(intensity, vertex) + data +end + +function _default(positions::VectorTypes{T}, s::style"linesegment", data::Dict) where T <: Point + @gen_defaults! data begin + vertex = positions => GLBuffer + color = default(RGBA, s, 1) => GLBuffer + color_map = nothing => Texture + color_norm = nothing + thickness = 2f0 => GLBuffer + shape = RECTANGLE + pattern = nothing + fxaa = false + indices = const_lift(length, positions) => to_index_buffer + # TODO update boundingbox + shader = GLVisualizeShader("fragment_output.frag", "util.vert", "line_segment.vert", "line_segment.geom", "lines.frag") + gl_primitive = GL_LINES + end + if !isa(pattern, Texture) && pattern != nothing + if !isa(pattern, Vector) + error("Pattern needs to be a Vector of floats") + end + tex = GLAbstraction.Texture(ticks(pattern, 100), x_repeat = :repeat) + data[:pattern] = tex + data[:pattern_length] = Float32(last(pattern)) + end + data +end diff --git a/GLMakie/src/GLVisualize/visualize/mesh.jl b/GLMakie/src/GLVisualize/visualize/mesh.jl new file mode 100644 index 00000000000..6e39a48094b --- /dev/null +++ b/GLMakie/src/GLVisualize/visualize/mesh.jl @@ -0,0 +1,20 @@ + +function _default(mesh::TOrSignal{M}, s::Style, data::Dict) where M <: GeometryBasics.Mesh + return @gen_defaults! data begin + shading = true + backlight = 0f0 + main = mesh + vertex_color = Vec4f0(0) + texturecoordinates = Vec2f0(0) + image = nothing => Texture + matcap = nothing => Texture + color_map = nothing => Texture + color_norm = nothing + fetch_pixel = false + uv_scale = Vec2f0(1) + shader = GLVisualizeShader( + "fragment_output.frag", "util.vert", "standard.vert", "standard.frag", + view = Dict("light_calc" => light_calc(shading)) + ) + end +end diff --git a/GLMakie/src/GLVisualize/visualize/particles.jl b/GLMakie/src/GLVisualize/visualize/particles.jl new file mode 100644 index 00000000000..e3e8f5c8aff --- /dev/null +++ b/GLMakie/src/GLVisualize/visualize/particles.jl @@ -0,0 +1,369 @@ +#= +A lot of visualization forms in GLVisualize are realised in the form of instanced +particles. This is because they can be handled very efficiently by OpenGL. +There are quite a few different ways to feed instances with different attributes. +The main constructor for particles is a tuple of (Primitive, Position), whereas +position can come in all forms and shapes. You can leave away the primitive. +In that case, GLVisualize will fill in some default that is anticipated to make +the most sense for the datatype. +=# + +#3D primitives +const Primitives3D = Union{AbstractGeometry{3}, AbstractMesh} +#2D primitives AKA sprites, since they are shapes mapped onto a 2D rectangle +const Sprites = Union{AbstractGeometry{2}, Shape, Char, Type} +const AllPrimitives = Union{AbstractGeometry, Shape, Char, AbstractMesh} + +using Makie: RectanglePacker + +# There is currently no way to get the two following two signatures +# under one function, which is why we delegate to meshparticle +function _default( + p::Tuple{TOrSignal{Pr}, VectorTypes{P}}, s::Style, data::Dict + ) where {Pr <: Primitives3D, P <: Point} + return meshparticle(p, s, data) +end + +function to_meshcolor(color::TOrSignal{Vector{T}}) where T <: Colorant + TextureBuffer(color) +end + +function to_meshcolor(color::TOrSignal{Matrix{T}}) where T <: Colorant + Texture(color) +end +function to_meshcolor(color) + color +end + +function to_mesh(mesh::TOrSignal{<: GeometryPrimitive}) + return NativeMesh(const_lift(GeometryBasics.normal_mesh, mesh)) +end + +function to_mesh(mesh::TOrSignal{<: GeometryBasics.Mesh}) + return NativeMesh(mesh) +end + +using Makie +using Makie: get_texture_atlas + +vec2quaternion(rotation::StaticVector{4}) = rotation + +function vec2quaternion(r::StaticVector{2}) + vec2quaternion(Vec3f0(r[1], r[2], 0)) +end +function vec2quaternion(rotation::StaticVector{3}) + Makie.rotation_between(Vec3f0(0, 0, 1), Vec3f0(rotation)) +end + +vec2quaternion(rotation::Vec4f0) = rotation +vec2quaternion(rotation::VectorTypes) = const_lift(x-> vec2quaternion.(x), rotation) +vec2quaternion(rotation::Node) = lift(vec2quaternion, rotation) +vec2quaternion(rotation::Makie.Quaternion)= Vec4f0(rotation.data) +vec2quaternion(rotation)= vec2quaternion(to_rotation(rotation)) +GLAbstraction.gl_convert(rotation::Makie.Quaternion)= Vec4f0(rotation.data) + + +""" +This is the main function to assemble particles with a GLNormalMesh as a primitive +""" +function meshparticle(p, s, data) + rot = get!(data, :rotation, Vec4f0(0, 0, 0, 1)) + rot = vec2quaternion(rot) + delete!(data, :rotation) + @gen_defaults! data begin + primitive = p[1] => to_mesh + position = p[2] => TextureBuffer + position_x = nothing => TextureBuffer + position_y = nothing => TextureBuffer + position_z = nothing => TextureBuffer + + scale = Vec3f0(1) => TextureBuffer + scale_x = nothing => TextureBuffer + scale_y = nothing => TextureBuffer + scale_z = nothing => TextureBuffer + + rotation = rot => TextureBuffer + texturecoordinates = nothing + shading = true + end + + @gen_defaults! data begin + color_map = nothing => Texture + color_norm = nothing + intensity = nothing + image = nothing + color = if color_map == nothing + default(RGBA{Float32}, s) + else + nothing + end => to_meshcolor + vertex_color = Vec4f0(1) + matcap = nothing => Texture + fetch_pixel = false + uv_scale = Vec2f0(1) + + instances = const_lift(length, position) + shading = true + backlight = 0f0 + shader = GLVisualizeShader( + "util.vert", "particles.vert", "fragment_output.frag", "standard.frag", + view = Dict( + "position_calc" => position_calc(position, position_x, position_y, position_z, TextureBuffer), + "light_calc" => light_calc(shading) + ) + ) + end + if Makie.to_value(intensity) != nothing + if Makie.to_value(position) != nothing + data[:intensity] = intensity_convert_tex(intensity, position) + data[:len] = const_lift(length, position) + else + data[:intensity] = intensity_convert_tex(intensity, position_x) + data[:len] = const_lift(length, position_x) + end + end + data +end + +to_pointsize(x::Number) = Float32(x) +to_pointsize(x) = Float32(x[1]) + +struct PointSizeRender + size::Observable +end +(x::PointSizeRender)() = glPointSize(to_pointsize(x.size[])) +""" +This is the most primitive particle system, which uses simple points as primitives. +This is supposed to be the fastest way of displaying particles! +""" +function _default(position::VectorTypes{T}, s::style"speed", data::Dict) where T <: Point + @gen_defaults! data begin + vertex = position => GLBuffer + color_map = nothing => Texture + color = (color_map == nothing ? default(RGBA{Float32}, s) : nothing) => GLBuffer + color_norm = nothing + scale = 2f0 + shader = GLVisualizeShader("fragment_output.frag", "dots.vert", "dots.frag") + gl_primitive = GL_POINTS + end + data[:prerender] = PointSizeRender(data[:scale]) + data +end + +""" +returns the Shape for the distancefield algorithm +""" +primitive_shape(::Char) = DISTANCEFIELD +primitive_shape(x::X) where {X} = primitive_shape(X) +primitive_shape(::Type{T}) where {T <: Circle} = CIRCLE +primitive_shape(::Type{T}) where {T <: Rect2D} = RECTANGLE +primitive_shape(x::Shape) = x + +""" +Extracts the scale from a primitive. +""" +primitive_scale(prim::GeometryPrimitive) = Vec2f0(widths(prim)) +primitive_scale(::Union{Shape, Char}) = Vec2f0(40) +primitive_scale(c) = Vec2f0(0.1) + +""" +Extracts the offset from a primitive. +""" +primitive_offset(x, scale::Nothing) = Vec2f0(0) # default offset +primitive_offset(x, scale) = const_lift(/, scale, -2f0) # default offset + + +""" +Extracts the uv offset and width from a primitive. +""" +primitive_uv_offset_width(c::Char) = glyph_uv_width!(c) +primitive_uv_offset_width(x) = Vec4f0(0,0,1,1) + +""" +Gets the texture atlas if primitive is a char. +""" +primitive_distancefield(x) = nothing +primitive_distancefield(::Char) = get_texture!(get_texture_atlas()) +primitive_distancefield(::Node{Char}) = get_texture!(get_texture_atlas()) + +function _default( + p::Tuple{TOrSignal{Matrix{C}}, VectorTypes{P}}, s::Style, data::Dict + ) where {C <: Colorant, P <: Point} + data[:image] = p[1] # we don't want this to be overwritten by user + @gen_defaults! data begin + scale = lift(x-> Vec2f0(size(x)), p[1]) + shape = RECTANGLE + offset = Vec2f0(0) + end + sprites(p, s, data) +end + +function _default( + p::Tuple{TOrSignal{Matrix{C}}, VectorTypes{P}}, s::Style, data::Dict + ) where {C <: AbstractFloat, P <: Point} + data[:distancefield] = p[1] # we don't want this to be overwritten by user + @gen_defaults! data begin + scale = lift(x-> Vec2f0(size(x)), p[1]) + shape = RECTANGLE + offset = Vec2f0(0) + end + sprites(p, s, data) +end + +function _default( + p::Tuple{VectorTypes{Matrix{C}}, VectorTypes{P}}, s::Style, data::Dict + ) where {C <: Colorant, P <: Point} + images = to_value(p[1]) + isempty(images) && error("Can not display empty vector of images as primitive") + sizes = map(size, images) + if !all(x-> x == sizes[1], sizes) # if differently sized + # create texture atlas + maxdims = sum(map(Vec{2, Int}, sizes)) + rectangles = map(x->Rect2D(0, 0, x...), sizes) + rpack = RectanglePacker(Rect2D(0, 0, maxdims...)) + uv_coordinates = [push!(rpack, rect).area for rect in rectangles] + max_xy = maximum(maximum.(uv_coordinates)) + texture_atlas = Texture(C, (max_xy...,)) + for (area, img) in zip(uv_coordinates, images) + texture_atlas[area] = img #transfer to texture atlas + end + data[:uv_offset_width] = map(uv_coordinates) do uv + m = max_xy .- 1 + mini = reverse((minimum(uv)) ./ m) + maxi = reverse((maximum(uv) .- 1) ./ m) + return Vec4f0(mini..., maxi...) + end + images = texture_atlas + end + data[:image] = images # we don't want this to be overwritten by user + @gen_defaults! data begin + shape = RECTANGLE + offset = Vec2f0(0) + end + sprites(p, s, data) +end + +# There is currently no way to get the two following two signatures +# under one function, which is why we delegate to sprites +_default(p::Tuple{TOrSignal{Pr}, VectorTypes{P}}, s::Style, data::Dict) where {Pr <: Sprites, P<:Point} = + sprites(p,s,data) + + +function _default( + p::Tuple{TOrSignal{Pr}, G}, s::Style, data::Dict + ) where {Pr <: Sprites, G <: Tuple} + @gen_defaults! data begin + shape = const_lift(primitive_shape, p[1]) + position = nothing => GLBuffer + position_x = p[2][1] => GLBuffer + position_y = p[2][2] => GLBuffer + position_z = length(p[2]) > 2 ? p[2][3] : 0f0 => GLBuffer + end + sprites(p, s, data) +end + +""" +Main assemble functions for sprite particles. +Sprites are anything like distance fields, images and simple geometries +""" +function sprites(p, s, data) + rot = get!(data, :rotation, Vec4f0(0, 0, 0, 1)) + rot = vec2quaternion(rot) + delete!(data, :rotation) + + @gen_defaults! data begin + shape = const_lift(x-> Int32(primitive_shape(x)), p[1]) + position = p[2] => GLBuffer + position_x = nothing => GLBuffer + position_y = nothing => GLBuffer + position_z = nothing => GLBuffer + + scale = const_lift(primitive_scale, p[1]) => GLBuffer + scale_x = nothing => GLBuffer + scale_y = nothing => GLBuffer + scale_z = nothing => GLBuffer + + rotation = rot => GLBuffer + image = nothing => Texture + end + # TODO don't make this dependant on some shady type dispatch + if isa(to_value(p[1]), Char) && !isa(to_value(scale), Union{StaticVector, AbstractVector{<: StaticVector}}) # correct dimensions + data[:scale] = const_lift(correct_scale, p[1], scale) + end + + @gen_defaults! data begin + offset = primitive_offset(p[1], scale) => GLBuffer + intensity = nothing => GLBuffer + color_map = nothing => Texture + color_norm = nothing + color = (color_map == nothing ? default(RGBA, s) : nothing) => GLBuffer + + glow_color = RGBA{Float32}(0,0,0,0) => GLBuffer + stroke_color = RGBA{Float32}(0,0,0,0) => GLBuffer + stroke_width = 0f0 + glow_width = 0f0 + uv_offset_width = const_lift(primitive_uv_offset_width, p[1]) => GLBuffer + + distancefield = primitive_distancefield(p[1]) => Texture + indices = const_lift(length, p[2]) => to_index_buffer + # rotation and billboard don't go along + billboard = rotation == Vec4f0(0,0,0,1) => "if `billboard` == true, particles will always face camera" + fxaa = false + shader = GLVisualizeShader( + "fragment_output.frag", "util.vert", "sprites.geom", + "sprites.vert", "distance_shape.frag", + view = Dict("position_calc"=>position_calc(position, position_x, position_y, position_z, GLBuffer)) + ) + scale_primitive = true + gl_primitive = GL_POINTS + end + # Exception for intensity, to make it possible to handle intensity with a + # different length compared to position. Intensities will be interpolated in that case + if position != nothing + data[:intensity] = intensity_convert(intensity, position) + data[:len] = const_lift(length, position) + else + data[:intensity] = intensity_convert(intensity, position_x) + data[:len] = const_lift(length, position_x) + end + return data +end + + +""" +Transforms text into a particle system of sprites, by inferring the +texture coordinates in the texture atlas, widths and positions of the characters. +""" +function _default(main::Tuple{TOrSignal{S}, P}, s::Style, data::Dict) where {S <: AbstractString, P} + data[:position] = main[2] + _default(main[1], s, data) +end + +function _default(main::TOrSignal{S}, s::Style, data::Dict) where S <: AbstractString + @gen_defaults! data begin + relative_scale = 4 # + start_position = Point2f0(0) + atlas = get_texture_atlas() + distancefield = get_texture!(atlas) + stroke_width = 0f0 + glow_width = 0f0 + font = to_font("default") + scale_primitive = true + position = const_lift(calc_position, main, start_position, relative_scale, font, atlas) + offset = const_lift(calc_offset, main, relative_scale, font, atlas) + prerender = () -> begin + glDisable(GL_DEPTH_TEST) + glDepthMask(GL_TRUE) + glDisable(GL_CULL_FACE) + enabletransparency() + end + uv_offset_width = const_lift(main) do str + Vec4f0[glyph_uv_width!(atlas, c, font) for c = str] + end + scale = const_lift(main, relative_scale) do str, s + Vec2f0[glyph_scale!(atlas, c, font, s) for c = str] + end + end + delete!(data, :font) + _default((DISTANCEFIELD, position), s, data) +end diff --git a/GLMakie/src/GLVisualize/visualize/surface.jl b/GLMakie/src/GLVisualize/visualize/surface.jl new file mode 100644 index 00000000000..a99f67cfaac --- /dev/null +++ b/GLMakie/src/GLVisualize/visualize/surface.jl @@ -0,0 +1,180 @@ + +# surface(::Matrix, ::Matrix, ::Matrix) +function _default(main::Tuple{MatTypes{T}, MatTypes{T}, MatTypes{T}}, s::Style{:surface}, data::Dict) where T <: AbstractFloat + @gen_defaults! data begin + position_x = main[1] => (Texture, "x position, must be a `Matrix{Float}`") + position_y = main[2] => (Texture, "y position, must be a `Matrix{Float}`") + position_z = main[3] => (Texture, "z position, must be a `Matrix{Float}`") + scale = Vec3f0(0) => "scale must be 0, for a surfacemesh" + end + surface(position_z, s, data) +end + +# surface(Vector or Range, Vector or Range, ::Matrix) +function _default(main::Tuple{VectorTypes{T}, VectorTypes{T}, MatTypes{T}}, s::Style{:surface}, data::Dict) where T <: AbstractFloat + @gen_defaults! data begin + position_x = main[1] => (Texture, "x position, must be a `Vector{Float}`") + position_y = main[2] => (Texture, "y position, must be a `Vector{Float}`") + position_z = main[3] => (Texture, "z position, must be a `Matrix{Float}`") + scale = Vec3f0(0) => "scale must be 0, for a surfacemesh" + end + surface(position_z, s, data) +end + +# surface(::Matrix) +function _default(main::MatTypes{T}, s::Style{:surface}, data::Dict) where T <: AbstractFloat + @gen_defaults! data begin + ranges = ((-1f0, 1f0), (-1f0,1f0)) => "x, and y position given as `(start, endvalue)` or any `Range`" + end + delete!(data, :ranges) # no need to have them in the OpenGL data + _default((Grid(to_value(main), to_value(ranges)), main), s, data) +end + +# surface(::Matrix) +function _default(main::Tuple{G, MatTypes{T}}, s::Style{:surface}, data::Dict) where {G <: Grid{2}, T <: AbstractFloat} + xrange = main[1].dims[1]; yrange = main[1].dims[2] + xscale = (maximum(xrange) - minimum(xrange)) / (length(xrange)-1) + yscale = (maximum(yrange) - minimum(yrange)) / (length(yrange)-1) + @gen_defaults! data begin + position = main[1] =>" Position given as a `Grid{2}`. + Can be constructed e.g. `Grid(LinRange(0,2,N1), LinRange(0,3, N2))`" + position_z = main[2] => (Texture, "height offset for the surface, must be `Matrix{Float}`") + scale = Vec3f0(xscale, yscale, 1) => "scale of the grid planes forming the surface. Can be made smaller, to let the grid show" + end + surface(position_z, s, data) +end + +_extrema(x::FRect3D) = Vec2f0(minimum(x)[3], maximum(x)[3]) +nothing_or_vec(x) = x +nothing_or_vec(x::Array) = vec(x) + +function normal_calc(x::Bool, invert_normals::Bool = false) + i = invert_normals ? "-" : "" + if x + "$(i)getnormal(position, position_x, position_y, position_z, o_uv);" + # "getnormal_fast(position_z, ind2sub(dims, index1D));" + else + "vec3(0, 0, $(i)1);" + end +end + +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); + """ + else + "" + end +end + +function native_triangle_mesh(mesh) + return gl_convert(triangle_mesh(mesh)) +end + +function surface(main, s::Style{:surface}, data::Dict) + + @gen_defaults! data begin + primitive = Rect2D(0f0,0f0,1f0,1f0) => native_triangle_mesh + scale = nothing + position = nothing + position_x = nothing => Texture + position_y = nothing => Texture + position_z = nothing => Texture + image = nothing => Texture + shading = true + normal = shading + invert_normals = false + backlight = 0f0 + end + @gen_defaults! data begin + color = nothing => Texture + color_map = nothing => Texture + color_norm = nothing + fetch_pixel = false + matcap = nothing => Texture + + nan_color = RGBAf0(1, 0, 0, 1) + highclip = RGBAf0(0, 0, 0, 0) + lowclip = RGBAf0(0, 0, 0, 0) + + uv_scale = Vec2f0(1) + instances = const_lift(x->(size(x,1)-1) * (size(x,2)-1), main) => "number of planes used to render the surface" + shader = GLVisualizeShader( + "fragment_output.frag", "util.vert", "surface.vert", + "standard.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), + ) + ) + end + return data +end + +function position_calc(x...) + _position_calc(Iterators.filter(x->!isa(x, Nothing), x)...) +end + +function _position_calc( + position_x::MatTypes{T}, position_y::MatTypes{T}, position_z::MatTypes{T}, target::Type{Texture} + ) where T<:AbstractFloat + """ + 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); + pos = vec3( + texelFetch(position_x, index2D, 0).x, + texelFetch(position_y, index2D, 0).x, + texelFetch(position_z, index2D, 0).x + ); + """ +end + +function _position_calc( + position_x::VectorTypes{T}, position_y::VectorTypes{T}, position_z::MatTypes{T}, + target::Type{Texture} + ) where T<:AbstractFloat + """ + 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); + pos = vec3( + texelFetch(position_x, index2D.x, 0).x, + texelFetch(position_y, index2D.y, 0).x, + texelFetch(position_z, index2D, 0).x + ); + """ +end + +function _position_calc( + position_xyz::VectorTypes{T}, target::Type{TextureBuffer} + ) where T <: StaticVector + "pos = texelFetch(position, index).xyz;" +end + +function _position_calc( + position_xyz::VectorTypes{T}, target::Type{GLBuffer} + ) where T <: StaticVector + len = length(T) + filler = join(ntuple(x->0, 3-len), ", ") + needs_comma = len != 3 ? ", " : "" + "pos = vec3(position $needs_comma $filler);" +end + +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)); + ivec2 index2D = ind2sub(dims, index1D); + vec2 index01 = vec2(index2D) / (vec2(dims)-1.0); + float height = texture(position_z, index01).x; + pos = vec3(grid_pos(position, index01), height); + """ +end diff --git a/GLMakie/src/GLVisualize/visualize_interface.jl b/GLMakie/src/GLVisualize/visualize_interface.jl new file mode 100644 index 00000000000..71245c3a21e --- /dev/null +++ b/GLMakie/src/GLVisualize/visualize_interface.jl @@ -0,0 +1,193 @@ +@enum Shape CIRCLE RECTANGLE ROUNDED_RECTANGLE DISTANCEFIELD TRIANGLE +@enum CubeSides TOP BOTTOM FRONT BACK RIGHT LEFT + +struct Grid{N,T <: AbstractRange} + dims::NTuple{N,T} +end +Base.ndims(::Grid{N,T}) where {N,T} = N + +Grid(ranges::AbstractRange...) = Grid(ranges) +function Grid(a::Array{T,N}) where {N,T} + s = Vec{N,Float32}(size(a)) + smax = maximum(s) + s = s ./ smax + Grid(ntuple(Val{N}) do i + range(0, stop=s[i], length=size(a, i)) + end) +end + +Grid(a::AbstractArray, ranges...) = Grid(a, ranges) + +""" +This constructor constructs a grid from ranges given as a tuple. +Due to the approach, the tuple `ranges` can consist of NTuple(2, T) +and all kind of range types. The constructor will make sure that all ranges match +the size of the dimension of the array `a`. +""" +function Grid(a::AbstractArray{T,N}, ranges::Tuple) where {T,N} + length(ranges) = ! N && throw(ArgumentError( + "You need to supply a range for every dimension of the array. Given: $ranges + given Array: $(typeof(a))" + )) + Grid(ntuple(Val(N)) do i + range(first(ranges[i]), stop=last(ranges[i]), length=size(a, i)) + end) +end + +Base.length(p::Grid) = prod(size(p)) +Base.size(p::Grid) = map(length, p.dims) +function Base.getindex(p::Grid{N,T}, i) where {N,T} + inds = ind2sub(size(p), i) + return Point{N,eltype(T)}(ntuple(Val(N)) do i + p.dims[i][inds[i]] + end) +end + +Base.iterate(g::Grid, i=1) = i <= length(g) ? (g[i], i + 1) : nothing + +GLAbstraction.isa_gl_struct(x::Grid) = true +GLAbstraction.toglsltype_string(t::Grid{N,T}) where {N,T} = "uniform Grid$(N)D" +function GLAbstraction.gl_convert_struct(g::Grid{N,T}, uniform_name::Symbol) where {N,T} + return Dict{Symbol,Any}( + Symbol("$uniform_name.start") => Vec{N,Float32}(minimum.(g.dims)), + Symbol("$uniform_name.stop") => Vec{N,Float32}(maximum.(g.dims)), + Symbol("$uniform_name.lendiv") => Vec{N,Cint}(length.(g.dims) .- 1), + Symbol("$uniform_name.dims") => Vec{N,Cint}(map(length, g.dims)) + ) +end +function GLAbstraction.gl_convert_struct(g::Grid{1,T}, uniform_name::Symbol) where T + x = g.dims[1] + return Dict{Symbol,Any}( + Symbol("$uniform_name.start") => Float32(minimum(x)), + Symbol("$uniform_name.stop") => Float32(maximum(x)), + Symbol("$uniform_name.lendiv") => Cint(length(x) - 1), + Symbol("$uniform_name.dims") => Cint(length(x)) + ) +end + +struct GLVisualizeShader <: AbstractLazyShader + paths::Tuple + kw_args::Dict{Symbol,Any} + function GLVisualizeShader(paths::String...; view=Dict{String,String}(), kw_args...) + # TODO properly check what extensions are available + @static if !Sys.isapple() + view["GLSL_EXTENSIONS"] = "#extension GL_ARB_conservative_depth: enable" + view["SUPPORTED_EXTENSIONS"] = "#define DETPH_LAYOUT" + end + view["buffers"] = get_buffers() + view["buffer_writes"] = get_buffer_writes() + args = Dict{Symbol, Any}(kw_args) + args[:view] = view + args[:fragdatalocation] = [(0, "fragment_color"), (1, "fragment_groupid")] + new(map(x -> loadshader(x), paths), args) + end +end + +function assemble_robj(data, program, bb, primitive, pre_fun, post_fun) + transp = get(data, :transparency, Node(false)) + overdraw = get(data, :overdraw, Node(false)) + pre = if pre_fun != nothing + _pre_fun = GLAbstraction.StandardPrerender(transp, overdraw) + function () + _pre_fun() + pre_fun() + end + else + GLAbstraction.StandardPrerender(transp, overdraw) + end + robj = RenderObject(data, program, pre, nothing, bb, nothing) + post = if haskey(data, :instances) + GLAbstraction.StandardPostrenderInstanced(data[:instances], robj.vertexarray, primitive) + else + GLAbstraction.StandardPostrender(robj.vertexarray, primitive) + end + robj.postrenderfunction = if post_fun !== nothing + () -> begin + post() + post_fun() + end + else + post + end + robj +end + +function assemble_shader(data) + shader = data[:shader] + delete!(data, :shader) + glp = get(data, :gl_primitive, GL_TRIANGLES) + return assemble_robj( + data, shader, FRect3D(), glp, + get(data, :prerender, nothing), + get(data, :postrender, nothing) + ) +end + +""" +Converts index arrays to the OpenGL equivalent. +""" +to_index_buffer(x::GLBuffer) = x +to_index_buffer(x::TOrSignal{Int}) = x +to_index_buffer(x::VecOrSignal{UnitRange{Int}}) = x +to_index_buffer(x::TOrSignal{UnitRange{Int}}) = x +""" +For integers, we transform it to 0 based indices +""" +to_index_buffer(x::AbstractVector{I}) where {I <: Integer} = indexbuffer(Cuint.(x .- 1)) +function to_index_buffer(x::Node{<: AbstractVector{I}}) where I <: Integer + indexbuffer(lift(x -> Cuint.(x .- 1), x)) +end + +""" +If already GLuint, we assume its 0 based (bad heuristic, should better be solved with some Index type) +""" +function to_index_buffer(x::VectorTypes{I}) where I <: Union{GLuint,LineFace{GLIndex}} + indexbuffer(x) +end + +to_index_buffer(x) = error( + "Not a valid index type: $(typeof(x)). + Please choose from Int, Vector{UnitRange{Int}}, Vector{Int} or a signal of either of them" +) + +""" +Creates a default visualization for any value. +The defaults can be customized via the key word arguments and the style parameter. +The style can change the the look completely (e.g points displayed as lines, or particles), +while the key word arguments just alter the parameters of one visualization. +Always returns a context, which can be displayed on a window via view(::Context, [display]). +""" +visualize(@nospecialize(main), s::Symbol=:default; kw_args...) = visualize(main, Style{s}(), Dict{Symbol,Any}(kw_args)) + + +function visualize(@nospecialize(main), @nospecialize(s), @nospecialize(data)) + data = _default(main, s, copy(data)) + @gen_defaults! data begin # make sure every object has these! + model = Mat4f0(I) + end + return assemble_shader(data) +end + +# Make changes to fragment_output to match what's needed for postprocessing +using ..GLMakie: enable_SSAO +function get_buffers() + if enable_SSAO[] + """ + layout(location=2) out vec4 fragment_position; + layout(location=3) out vec3 fragment_normal_occlusion; + """ + else + "" + end +end + +function get_buffer_writes() + if enable_SSAO[] + """ + fragment_position = o_view_pos; + fragment_normal_occlusion.xyz = o_normal; + """ + else + "" + end +end diff --git a/GLMakie/src/display.jl b/GLMakie/src/display.jl new file mode 100644 index 00000000000..84e47273b1d --- /dev/null +++ b/GLMakie/src/display.jl @@ -0,0 +1,64 @@ +function Makie.backend_display(::GLBackend, scene::Scene) + screen = global_gl_screen(size(scene), Makie.use_display[]) + display_loading_image(screen) + Makie.backend_display(screen, scene) + return screen +end + +function Makie.backend_display(screen::Screen, scene::Scene) + empty!(screen) + # So, the GLFW window events are not guarantee to fire + # when we close a window, so we ensure this here! + window_open = events(scene).window_open + on(screen.window_open) do open + window_open[] = open + end + register_callbacks(scene, screen) + pollevents(screen) + insertplots!(screen, scene) + pollevents(screen) + return +end + +""" + scene2image(scene::Scene) + +Buffers the `scene` in an image buffer. +""" +function scene2image(scene::Scene) + old = WINDOW_CONFIG.pause_rendering[] + try + WINDOW_CONFIG.pause_rendering[] = true + screen = global_gl_screen(size(scene), false) + Makie.backend_display(screen, scene) + return Makie.colorbuffer(screen), screen + finally + WINDOW_CONFIG.pause_rendering[] = old + end +end + +raw_io(io::IO) = io +raw_io(io::IOContext) = raw_io(io.io) + +function Makie.backend_show(::GLBackend, io::IO, m::MIME"image/png", scene::Scene) + img, screen = scene2image(scene) + # TODO: when FileIO 1.6 is the minimum required version, delete the conditional + if isdefined(FileIO, :action) # FileIO 1.6+ + # keep this one + FileIO.save(FileIO.Stream{FileIO.format"PNG"}(raw_io(io)), img) + else + # delete this one + FileIO.save(FileIO.Stream(FileIO.format"PNG", raw_io(io)), img) + end + return screen +end + +function Makie.backend_show(::GLBackend, io::IO, m::MIME"image/jpeg", scene::Scene) + img, screen = scene2image(scene) + if isdefined(FileIO, :action) # FileIO 1.6+ + FileIO.save(FileIO.Stream{FileIO.format"JPEG"}(raw_io(io)), img) + else + FileIO.save(FileIO.Stream(FileIO.format"JPEG", raw_io(io)), img) + end + return screen +end diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl new file mode 100644 index 00000000000..dbd1b7ea06b --- /dev/null +++ b/GLMakie/src/drawing_primitives.jl @@ -0,0 +1,512 @@ +using Makie: get_texture_atlas, glyph_uv_width!, transform_func_obs, apply_transform +using Makie: attribute_per_char, FastPixel, el32convert, Pixel +using Makie: convert_arguments, preprojected_glyph_arrays + +convert_attribute(s::ShaderAbstractions.Sampler{RGBAf0}, k::key"color") = s +function convert_attribute(s::ShaderAbstractions.Sampler{T, N}, k::key"color") where {T, N} + ShaderAbstractions.Sampler( + el32convert(s.data), minfilter = s.minfilter, magfilter = s.magfilter, + x_repeat = s.repeat[1], y_repeat = s.repeat[min(2, N)], z_repeat = s.repeat[min(3, N)], + anisotropic = s.anisotropic, color_swizzel = s.color_swizzel + ) +end + +gpuvec(x) = GPUVector(GLBuffer(x)) + +to_range(x, y) = to_range.((x, y)) +to_range(x::ClosedInterval) = (minimum(x), maximum(x)) +to_range(x::VecTypes{2}) = x +to_range(x::AbstractRange) = (minimum(x), maximum(x)) +to_range(x::AbstractVector) = (minimum(x), maximum(x)) + +function to_range(x::AbstractArray) + if length(x) in size(x) # assert that just one dim != 1 + to_range(vec(x)) + else + error("Can't convert to a range. Please supply a range/vector/interval or a tuple (min, max)") + end +end + +function to_glvisualize_key(k) + k == :rotations && return :rotation + k == :markersize && return :scale + k == :glowwidth && return :glow_width + k == :glowcolor && return :glow_color + k == :strokewidth && return :stroke_width + k == :strokecolor && return :stroke_color + k == :positions && return :position + k == :linewidth && return :thickness + k == :marker_offset && return :offset + k == :colormap && return :color_map + k == :colorrange && return :color_norm + k == :transform_marker && return :scale_primitive + return k +end + +make_context_current(screen::Screen) = GLFW.MakeContextCurrent(to_native(screen)) + +function cached_robj!(robj_func, screen, scene, x::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) + !(k in (:transformation, :tickranges, :ticklabels, :raw, :SSAO, :lightposition)) + 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_key => gl_value + end) + + if haskey(gl_attributes, :markerspace) + mspace = pop!(gl_attributes, :markerspace) + gl_attributes[:use_pixel_marker] = lift(x-> x <: Pixel, mspace) + end + + if haskey(x.attributes, :lightposition) + eyepos = scene.camera.eyeposition + gl_attributes[:lightposition] = lift(x.attributes[:lightposition], eyepos) do pos, eyepos + return pos == :eyeposition ? eyepos : pos + end + end + robj = robj_func(gl_attributes) + for key in (:pixel_space, :view, :projection, :resolution, :eyeposition, :projectionview) + if !haskey(robj.uniforms, key) + robj[key] = getfield(scene.camera, key) + end + end + + if !haskey(gl_attributes, :normalmatrix) + robj[:normalmatrix] = map(robj[:view], robj[:model]) do v, m + i = SOneTo(3) + return transpose(inv(v[i, i] * m[i, i])) + end + end + + !haskey(gl_attributes, :ssao) && (robj[:ssao] = Node(false)) + screen.cache2plot[robj.id] = x + robj + end + push!(screen, scene, robj) + robj +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 +handle_view(array::Node, attributes) = array + +function handle_view(array::SubArray, attributes) + A = parent(array) + indices = index1D(array) + attributes[:indices] = indices + return A +end + +function handle_view(array::Node{T}, attributes) where T <: SubArray + A = lift(parent, array) + indices = lift(index1D, array) + attributes[:indices] = indices + return A +end + +function lift_convert(key, value, plot) + return lift_convert_inner(value, Key{key}(), Key{Makie.plotkey(plot)}(), plot) +end + +function lift_convert_inner(value, key, plot_key, plot) + return lift(value) do value + return convert_attribute(value, key, plot_key) + end +end + +to_vec4(val::RGB) = RGBAf0(val, 1.0) +to_vec4(val::RGBA) = RGBAf0(val) + +function lift_convert_inner(value, ::key"highclip", plot_key, plot) + return lift(value, plot.colormap) do value, cmap + val = value === nothing ? to_colormap(cmap)[end] : to_color(value) + return to_vec4(val) + end +end + +function lift_convert_inner(value, ::key"lowclip", plot_key, plot) + return lift(value, plot.colormap) do value, cmap + val = value === nothing ? to_colormap(cmap)[1] : to_color(value) + return to_vec4(val) + end +end + +pixel2world(scene, msize::Number) = pixel2world(scene, Point2f0(msize))[1] + +function pixel2world(scene, msize::StaticVector{2}) + # TODO figure out why Vec(x, y) doesn't work correctly + p0 = Makie.to_world(scene, Point2f0(0.0)) + p1 = Makie.to_world(scene, Point2f0(msize)) + diff = p1 - p0 + return diff +end + +pixel2world(scene, msize::AbstractVector) = pixel2world.(scene, msize) + +function handle_intensities!(attributes) + if haskey(attributes, :color) && attributes[:color][] isa AbstractVector{<: Number} + c = pop!(attributes, :color) + attributes[:intensity] = lift(x-> convert(Vector{Float32}, x), c) + else + delete!(attributes, :intensity) + delete!(attributes, :color_map) + delete!(attributes, :color_norm) + end +end + +function Base.insert!(screen::GLScreen, scene::Scene, @nospecialize(x::Combined)) + # poll inside functions to make wait on compile less prominent + pollevents(screen) + if isempty(x.plots) # if no plots inserted, this truly is an atomic + draw_atomic(screen, scene, x) + else + foreach(x.plots) do x + # poll inside functions to make wait on compile less prominent + pollevents(screen) + insert!(screen, scene, x) + end + end +end + +function draw_atomic(screen::GLScreen, scene::Scene, @nospecialize(x::Union{Scatter, MeshScatter})) + robj = cached_robj!(screen, scene, x) 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) + if isa(x, Scatter) + gl_attributes[:billboard] = map(rot-> isa(rot, Billboard), x.rotations) + gl_attributes[:distancefield][] == nothing && delete!(gl_attributes, :distancefield) + gl_attributes[:uv_offset_width][] == Vec4f0(0) && delete!(gl_attributes, :uv_offset_width) + end + + positions = handle_view(x[1], gl_attributes) + positions = apply_transform(transform_func_obs(x), positions) + + if marker[] isa FastPixel + filter!(gl_attributes) do (k, v,) + k in (:color_map, :color, :color_norm, :scale, :fxaa, :model) + end + if !(gl_attributes[:color][] isa AbstractVector{<: Number}) + delete!(gl_attributes, :color_norm) + delete!(gl_attributes, :color_map) + end + visualize(positions, Style(:speed), Dict{Symbol, Any}(gl_attributes)) + else + handle_intensities!(gl_attributes) + visualize((marker, positions), Style(:default), Dict{Symbol, Any}(gl_attributes)) + end + end +end + +function draw_atomic(screen::GLScreen, scene::Scene, @nospecialize(x::Lines)) + robj = cached_robj!(screen, scene, x) do gl_attributes + linestyle = pop!(gl_attributes, :linestyle) + data = Dict{Symbol, Any}(gl_attributes) + ls = to_value(linestyle) + if isnothing(ls) + data[:pattern] = ls + else + linewidth = gl_attributes[:thickness] + data[:pattern] = ls .* (to_value(linewidth) * 0.25) + end + positions = handle_view(x[1], data) + positions = apply_transform(transform_func_obs(x), positions) + handle_intensities!(data) + visualize(positions, Style(:lines), data) + end +end + +function draw_atomic(screen::GLScreen, scene::Scene, @nospecialize(x::LineSegments)) + robj = cached_robj!(screen, scene, x) do gl_attributes + linestyle = pop!(gl_attributes, :linestyle) + data = Dict{Symbol, Any}(gl_attributes) + ls = to_value(linestyle) + if isnothing(ls) + data[:pattern] = ls + else + linewidth = gl_attributes[:thickness] + data[:pattern] = ls .* (to_value(linewidth) * 0.25) + end + positions = handle_view(x.converted[1], data) + positions = apply_transform(transform_func_obs(x), positions) + if haskey(data, :color) && data[:color][] isa AbstractVector{<: Number} + c = pop!(data, :color) + data[:color] = el32convert(c) + else + delete!(data, :color_map) + delete!(data, :color_norm) + end + visualize(positions, Style(:linesegment), data) + end +end + +value_or_first(x::AbstractArray) = first(x) +value_or_first(x::StaticArray) = x +value_or_first(x) = x + +function draw_atomic(screen::GLScreen, scene::Scene, x::Text) + robj = cached_robj!(screen, scene, x) do gl_attributes + string_obs = x[1] + liftkeys = (:position, :textsize, :font, :align, :rotation, :model, :justification, :lineheight, :space, :offset) + args = getindex.(Ref(gl_attributes), liftkeys) + + gl_text = lift(string_obs, scene.camera.projectionview, Makie.transform_func_obs(scene), args...) do str, projview, transfunc, pos, tsize, font, align, rotation, model, j, l, space, offset + # For annotations, only str (x[1]) will get updated, but all others are updated too! + args = @get_attribute x (position, textsize, font, align, rotation, offset) + res = Vec2f0(widths(pixelarea(scene)[])) + return preprojected_glyph_arrays(str, pos, x._glyphlayout[], font, textsize, space, projview, res, offset, transfunc) + end + + # unpack values from the one signal: + positions, offset, uv_offset_width, scale = map((1, 2, 3, 4)) do i + lift(getindex, gl_text, i) + end + + atlas = get_texture_atlas() + keys = (:color, :strokecolor, :rotation) + + signals = map(keys) do key + return lift(positions, x[key]) do pos, attr + str = string_obs[] + if str isa AbstractVector + if isempty(str) + attr = convert_attribute(value_or_first(attr), Key{key}()) + return Vector{typeof(attr)}() + else + result = [] + broadcast_foreach(str, attr) do st, aa + for att in attribute_per_char(st, aa) + push!(result, convert_attribute(att, Key{key}())) + end + end + # narrow the type from any, this is ugly + return identity.(result) + end + else + return Makie.get_attribute(x, key) + end + end + end + + filter!(gl_attributes) do (k, v) + # These are liftkeys without model but with _glyphlayout + !(k in ( + :position, :space, :justification, :font, :_glyphlayout, :align, + :textsize, :rotation, :lineheight, + )) + end + gl_attributes[:color] = signals[1] + gl_attributes[:stroke_color] = signals[2] + gl_attributes[:rotation] = signals[3] + gl_attributes[:scale] = scale + gl_attributes[:offset] = offset + gl_attributes[:uv_offset_width] = uv_offset_width + gl_attributes[:distancefield] = get_texture!(atlas) + + + robj = visualize((DISTANCEFIELD, positions), Style(:default), gl_attributes) + + # Draw text in screenspace + if x.space[] == :screen + robj[:view] = Observable(Mat4f0(I)) + robj[:projection] = scene.camera.pixel_space + robj[:projectionview] = scene.camera.pixel_space + end + + return robj + end + return robj +end + +xy_convert(x::AbstractVector, n) = el32convert(x) +xy_convert(x, n) = Float32[LinRange(extrema(x)..., n + 1);] + +function draw_atomic(screen::GLScreen, scene::Scene, x::Heatmap) + return cached_robj!(screen, scene, x) do gl_attributes + t = Makie.transform_func_obs(scene) + mat = x[3] + xpos = map(t, x[1]) do t, x + n = size(mat[], 1) + return first.(apply_transform.((t,), Point.(xy_convert(x, n), 0))) + end + ypos = map(t, x[2]) do t, y + n = size(mat[], 1) + return last.(apply_transform.((t,), Point.(0, xy_convert(y, n)))) + end + 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 + (length(x)-1) * (length(y)-1) + end + interp = to_value(pop!(gl_attributes, :interpolate)) + interp = interp ? :linear : :nearest + if !(to_value(mat) isa ShaderAbstractions.Sampler) + tex = Texture(el32convert(mat), minfilter = interp) + else + tex = to_value(mat) + end + pop!(gl_attributes, :color) + gl_attributes[:stroke_width] = pop!(gl_attributes, :thickness) + # gl_attributes[:color_map] = Texture(gl_attributes[:color_map], minfilter=:nearest) + GLVisualize.assemble_shader(GLVisualize.gl_heatmap(tex, gl_attributes)) + end +end + +function vec2color(colors, cmap, crange) + Makie.interpolated_getindex.((to_colormap(cmap),), colors, (crange,)) +end + +function get_image(plot) + if isa(plot[:color][], AbstractMatrix{<: Number}) + lift(vec2color, pop!.(Ref(plot), (:color, :color_map, :color_norm))...) + else + delete!(plot, :color_norm) + delete!(plot, :color_map) + return pop!(plot, :color) + end +end + +function draw_atomic(screen::GLScreen, scene::Scene, x::Image) + robj = cached_robj!(screen, scene, x) do gl_attributes + gl_attributes[:ranges] = lift(to_range, x[1], x[2]) + img = get_image(gl_attributes) + interp = to_value(pop!(gl_attributes, :interpolate)) + interp = interp ? :linear : :nearest + tex = Texture(el32convert(img), minfilter = interp) + visualize(tex, Style(:default), gl_attributes) + end +end + +convert_mesh_color(c::AbstractArray{<: Number}, cmap, crange) = vec2color(c, cmap, crange) +convert_mesh_color(c, cmap, crange) = c + +function update_positions(mesh::GeometryBasics.Mesh, positions) + points = coordinates(mesh) + attr = GeometryBasics.attributes(points) + delete!(attr, :position) # position == metafree(points) + return GeometryBasics.Mesh(meta(positions; attr...), faces(mesh)) +end + +function draw_atomic(screen::GLScreen, scene::Scene, meshplot::Mesh) + robj = cached_robj!(screen, scene, meshplot) do gl_attributes + # signals not supported for shading yet + gl_attributes[:shading] = to_value(pop!(gl_attributes, :shading)) + color = pop!(gl_attributes, :color) + # cmap = get(gl_attributes, :color_map, Node(nothing)); delete!(gl_attributes, :color_map) + # crange = get(gl_attributes, :color_norm, Node(nothing)); delete!(gl_attributes, :color_norm) + mesh = meshplot[1] + + if to_value(color) isa Colorant + gl_attributes[:vertex_color] = color + 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) + gl_attributes[:image] = ShaderAbstractions.Sampler(img, x_repeat=:repeat, minfilter=:nearest) + haskey(gl_attributes, :fetch_pixel) || (gl_attributes[:fetch_pixel] = true) + elseif to_value(color) isa AbstractMatrix{<:Colorant} + gl_attributes[:image] = color + delete!(gl_attributes, :color_map) + delete!(gl_attributes, :color_norm) + elseif to_value(color) isa AbstractMatrix{<: Number} + cmap = pop!(gl_attributes, :color_map) + crange = pop!(gl_attributes, :color_norm) + mesh = lift(mesh, color, cmap, crange) do mesh, color, cmap, crange + color_sampler = convert_mesh_color(color, cmap, crange) + mesh, uv = GeometryBasics.pop_pointmeta(mesh, :uv) + uv_sampler = Makie.sampler(color_sampler, uv) + return GeometryBasics.pointmeta(mesh, color=uv_sampler) + end + elseif to_value(color) isa AbstractVector{<: Union{Number, Colorant}} + mesh = lift(mesh, color) do mesh, color + return GeometryBasics.pointmeta(mesh, color=el32convert(color)) + end + end + + mesh = map(mesh, transform_func_obs(meshplot)) do mesh, func + if func ∉ (identity, (identity, identity), (identity, identity, identity)) + return update_positions(mesh, apply_transform.(Ref(func), mesh.position)) + end + return mesh + end + visualize(mesh, Style(:default), gl_attributes) + end +end + +function draw_atomic(screen::GLScreen, scene::Scene, x::Surface) + robj = cached_robj!(screen, scene, x) 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 isa(to_value(color), AbstractMatrix{<: Number}) && to_value(color) !== to_value(x[3]) + img = el32convert(color) + elseif to_value(color) isa Makie.AbstractPattern + pattern_img = lift(x -> el32convert(Makie.to_image(x)), 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 + gl_attributes[:color] = nothing + gl_attributes[:color_norm] = nothing + elseif isa(to_value(color), AbstractMatrix{<: Colorant}) + img = color + gl_attributes[:color_map] = nothing + gl_attributes[:color] = nothing + gl_attributes[:color_norm] = nothing + end + + 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]) + + if all(T -> T <: Union{AbstractMatrix, AbstractVector}, types) + args = map(x[1:3]) do arg + Texture(el32convert(arg); minfilter=:nearest) + end + return visualize(args, Style(:surface), gl_attributes) + else + gl_attributes[:ranges] = to_range.(to_value.(x[1:2])) + z_data = Texture(el32convert(x[3]); minfilter=:nearest) + return visualize(z_data, Style(:surface), gl_attributes) + end + end + return robj +end + +function draw_atomic(screen::GLScreen, 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... + mi = minimum.(xyz) + maxi = maximum.(xyz) + w = maxi .- mi + m2 = Mat4f0( + w[1], 0, 0, 0, + 0, w[2], 0, 0, + 0, 0, w[3], 0, + mi[1], mi[2], mi[3], 1 + ) + return convert(Mat4f0, m) * m2 + end + return visualize(vol[4], Style(:default), gl_attributes) + end +end diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl new file mode 100644 index 00000000000..d9f2d428895 --- /dev/null +++ b/GLMakie/src/events.jl @@ -0,0 +1,289 @@ +using Makie: MouseButtonEvent, KeyEvent + +macro print_error(expr) + return quote + try + $(esc(expr)) + catch e + println(stderr, "Error in callback:") + # TODO is it fine to call catch_backtrace inside C call? + Base.showerror(stderr, e, Base.catch_backtrace()) + println(stderr) + end + end +end + +""" +Returns a signal, which is true as long as the window is open. +returns `Node{Bool}` +[GLFW Docs](http://www.glfw.org/docs/latest/group__window.html#gaade9264e79fae52bdb78e2df11ee8d6a) +""" +window_open(scene::Scene, screen) = window_open(scene, to_native(screen)) +function window_open(scene::Scene, window::GLFW.Window) + event = scene.events.window_open + function windowclose(win) + @print_error begin + event[] = false + end + end + disconnect!(window, window_open) + event[] = isopen(window) + GLFW.SetWindowCloseCallback(window, windowclose) +end + +function 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 window_area(scene::Scene, screen::Screen) + window = to_native(screen) + event = scene.events.window_area + dpievent = scene.events.window_dpi + + disconnect!(window, window_area) + monitor = GLFW.GetPrimaryMonitor() + props = MonitorProperties(monitor) + dpievent[] = minimum(props.dpi) + + on(screen.render_tick) do _ + rect = event[] + # 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[] = IRect(x, y, framebuffer_size(window)) + # end + w, h = GLFW.GetFramebufferSize(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 + dpievent[] = minimum(props.dpi) + event[] = IRect(minimum(rect), w, h) + end + end + return +end + +function disconnect!(window::GLFW.Window, ::typeof(window_area)) + GLFW.SetWindowPosCallback(window, nothing) + GLFW.SetFramebufferSizeCallback(window, nothing) +end + + +""" +Registers a callback for the mouse buttons + modifiers +returns `Node{NTuple{4, Int}}` +[GLFW Docs](http://www.glfw.org/docs/latest/group__input.html#ga1e008c7a8751cea648c8f42cc91104cf) +""" +mouse_buttons(scene::Scene, screen) = mouse_buttons(scene, to_native(screen)) +function mouse_buttons(scene::Scene, window::GLFW.Window) + event = scene.events.mousebutton + function mousebuttons(window, button, action, mods) + @print_error begin + event[] = MouseButtonEvent(Mouse.Button(Int(button)), Mouse.Action(Int(action))) + end + end + disconnect!(window, mouse_buttons) + GLFW.SetMouseButtonCallback(window, mousebuttons) +end +function disconnect!(window::GLFW.Window, ::typeof(mouse_buttons)) + GLFW.SetMouseButtonCallback(window, nothing) +end +keyboard_buttons(scene::Scene, screen) = keyboard_buttons(scene, to_native(screen)) +function keyboard_buttons(scene::Scene, window::GLFW.Window) + event = scene.events.keyboardbutton + function keyoardbuttons(window, button, scancode::Cint, action, mods::Cint) + @print_error begin + event[] = KeyEvent(Keyboard.Button(Int(button)), Keyboard.Action(Int(action))) + end + end + disconnect!(window, keyboard_buttons) + GLFW.SetKeyCallback(window, keyoardbuttons) +end + +function disconnect!(window::GLFW.Window, ::typeof(keyboard_buttons)) + GLFW.SetKeyCallback(window, nothing) +end + +""" +Registers a callback for drag and drop of files. +returns `Node{Vector{String}}`, which are absolute file paths +[GLFW Docs](http://www.glfw.org/docs/latest/group__input.html#gacc95e259ad21d4f666faa6280d4018fd) +""" +dropped_files(scene::Scene, screen) = dropped_files(scene, to_native(screen)) +function dropped_files(scene::Scene, window::GLFW.Window) + event = scene.events.dropped_files + function droppedfiles(window, files) + @print_error begin + event[] = String.(files) + end + end + disconnect!(window, dropped_files) + event[] = String[] + GLFW.SetDropCallback(window, droppedfiles) +end +function disconnect!(window::GLFW.Window, ::typeof(dropped_files)) + GLFW.SetDropCallback(window, nothing) +end + +""" +Registers a callback for keyboard unicode input. +returns an `Node{Vector{Char}}`, +containing the pressed char. Is empty, if no key is pressed. +[GLFW Docs](http://www.glfw.org/docs/latest/group__input.html#ga1e008c7a8751cea648c8f42cc91104cf) +""" +unicode_input(scene::Scene, screen) = unicode_input(scene, to_native(screen)) +function unicode_input(scene::Scene, window::GLFW.Window) + event = scene.events.unicode_input + function unicodeinput(window, c::Char) + @print_error begin + event[] = c + end + end + disconnect!(window, unicode_input) + # x = Char[]; sizehint!(x, 1) + # event[] = x + GLFW.SetCharCallback(window, unicodeinput) +end +function 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])) +end + +""" +Registers a callback for the mouse cursor position. +returns an `Node{Vec{2, Float64}}`, +which is not in scene coordinates, with the upper left window corner being 0 +[GLFW Docs](http://www.glfw.org/docs/latest/group__input.html#ga1e008c7a8751cea648c8f42cc91104cf) +""" +function mouse_position(scene::Scene, screen::Screen) + window = to_native(screen) + e = events(scene) + on(screen.render_tick) do _ + !e.hasfocus[] && return + x, y = GLFW.GetCursorPos(window) + pos = correct_mouse(window, x, y) + if pos != e.mouseposition[] + @print_error e.mouseposition[] = pos + # notify!(e.mouseposition) + end + return + end + + # function cursorposition(window, w::Cdouble, h::Cdouble) + # @print_error begin + # pos = correct_mouse(window, w, h) + # @timeit "triggerless mouseposition" begin + # e.mouseposition.val = pos + # end + # return + # end + # end + # disconnect!(window, mouse_position) + # GLFW.SetCursorPosCallback(window, cursorposition) + + return +end +function disconnect!(window::GLFW.Window, ::typeof(mouse_position)) + GLFW.SetCursorPosCallback(window, nothing) + nothing +end + +""" +Registers a callback for the mouse scroll. +returns an `Node{Vec{2, Float64}}`, +which is an x and y offset. +[GLFW Docs](http://www.glfw.org/docs/latest/group__input.html#gacc95e259ad21d4f666faa6280d4018fd) +""" +scroll(scene::Scene, screen) = scroll(scene, to_native(screen)) +function scroll(scene::Scene, window::GLFW.Window) + event = scene.events.scroll + function scrollcb(window, w::Cdouble, h::Cdouble) + @print_error begin + event[] = (w, h) + end + end + disconnect!(window, scroll) + GLFW.SetScrollCallback(window, scrollcb) +end +function disconnect!(window::GLFW.Window, ::typeof(scroll)) + GLFW.SetScrollCallback(window, nothing) +end + +""" +Registers a callback for the focus of a window. +returns an `Node{Bool}`, +which is true whenever the window has focus. +[GLFW Docs](http://www.glfw.org/docs/latest/group__window.html#ga6b5f973531ea91663ad707ba4f2ac104) +""" +hasfocus(scene::Scene, screen) = hasfocus(scene, to_native(screen)) +function hasfocus(scene::Scene, window::GLFW.Window) + event = scene.events.hasfocus + function hasfocuscb(window, focus::Bool) + @print_error begin + event[] = focus + end + end + disconnect!(window, hasfocus) + GLFW.SetWindowFocusCallback(window, hasfocuscb) + event[] = GLFW.GetWindowAttrib(window, GLFW.FOCUSED) + nothing +end +function disconnect!(window::GLFW.Window, ::typeof(hasfocus)) + GLFW.SetWindowFocusCallback(window, nothing) +end + +""" +Registers a callback for if the mouse has entered the window. +returns an `Node{Bool}`, +which is true whenever the cursor enters the window. +[GLFW Docs](http://www.glfw.org/docs/latest/group__input.html#ga762d898d9b0241d7e3e3b767c6cf318f) +""" +entered_window(scene::Scene, screen) = entered_window(scene, to_native(screen)) +function entered_window(scene::Scene, window::GLFW.Window) + event = scene.events.entered_window + function enteredwindowcb(window, entered::Bool) + @print_error begin + event[] = entered + end + end + disconnect!(window, entered_window) + GLFW.SetCursorEnterCallback(window, enteredwindowcb) +end + +function disconnect!(window::GLFW.Window, ::typeof(entered_window)) + GLFW.SetCursorEnterCallback(window, nothing) +end diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl new file mode 100644 index 00000000000..23ace74a6da --- /dev/null +++ b/GLMakie/src/gl_backend.jl @@ -0,0 +1,71 @@ +try + using GLFW +catch e + @warn(""" + OpenGL/GLFW wasn't loaded correctly or couldn't be initialized. + This likely means, you're on a headless server without having OpenGL support setup correctly. + Have a look at the troubleshooting section in the readme: + https://github.com/JuliaPlots/Makie.jl/tree/master/GLMakie#troubleshooting-opengl. + """) + rethrow(e) +end + +include("GLAbstraction/GLAbstraction.jl") + +using .GLAbstraction + +const atlas_texture_cache = Dict{Any, Tuple{Texture{Float16, 2}, Function}}() + +function get_texture!(atlas) + Makie.set_glyph_resolution!(Makie.High) + # clean up dead context! + filter!(atlas_texture_cache) do (ctx, tex_func) + if GLAbstraction.context_alive(ctx) + return true + else + Makie.remove_font_render_callback!(tex_func[2]) + return false + end + end + tex, func = get!(atlas_texture_cache, GLAbstraction.current_context()) do + tex = Texture( + atlas.data, + minfilter = :linear, + magfilter = :linear, + # TODO: Consider alternatives to using the builtin anisotropic + # samplers for signed distance fields; the anisotropic + # filtering should happen *after* the SDF thresholding, but + # with the builtin sampler it happens before. + anisotropic = 16f0, + mipmap = true + ) + # update the texture, whenever a new font is added to the atlas + function callback(distance_field, rectangle) + ctx = tex.context + if GLAbstraction.context_alive(ctx) + prev_ctx = GLAbstraction.current_context() + ShaderAbstractions.switch_context!(ctx) + tex[rectangle] = distance_field + ShaderAbstractions.switch_context!(prev_ctx) + end + end + Makie.font_render_callback!(callback) + return (tex, callback) + end + return tex +end + +# TODO +const enable_SSAO = Ref(false) +const enable_FXAA = Ref(true) + +include("GLVisualize/GLVisualize.jl") +using .GLVisualize + +include("glwindow.jl") +include("postprocessing.jl") +include("screen.jl") +include("rendering.jl") +include("events.jl") +include("drawing_primitives.jl") +include("display.jl") diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl new file mode 100644 index 00000000000..022fca639d8 --- /dev/null +++ b/GLMakie/src/glwindow.jl @@ -0,0 +1,155 @@ +""" +Selection of random objects on the screen is realized by rendering an +object id + plus an arbitrary index into the framebuffer. +The index can be used for e.g. instanced geometries. +""" +struct SelectionID{T <: Integer} <: FieldVector{2, T} + id::T + index::T +end + + +mutable struct GLFramebuffer + resolution::Node{NTuple{2, Int}} + id::NTuple{2, GLuint} + + buffers::Dict{Symbol, Texture} + render_buffer_ids::Vector{GLuint} +end + +# it's guaranteed, that they all have the same size +Base.size(fb::GLFramebuffer) = size(fb.buffers[:color]) + + +function attach_framebuffer(t::Texture{T, 2}, attachment) where T + glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, t.id, 0) +end + +function GLFramebuffer(fb_size::NTuple{2, Int}) + # First Framebuffer + render_framebuffer = glGenFramebuffers() + glBindFramebuffer(GL_FRAMEBUFFER, render_framebuffer) + + color_buffer = Texture(RGBA{N0f8}, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge) + objectid_buffer = Texture(Vec{2, GLuint}, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge) + + depth_buffer = Texture( + Ptr{GLAbstraction.DepthStencil_24_8}(C_NULL), fb_size, + minfilter = :nearest, x_repeat = :clamp_to_edge, + internalformat = GL_DEPTH24_STENCIL8, + format = GL_DEPTH_STENCIL + ) + + attach_framebuffer(color_buffer, GL_COLOR_ATTACHMENT0) + attach_framebuffer(objectid_buffer, GL_COLOR_ATTACHMENT1) + attach_framebuffer(depth_buffer, GL_DEPTH_ATTACHMENT) + attach_framebuffer(depth_buffer, GL_STENCIL_ATTACHMENT) + + status = glCheckFramebufferStatus(GL_FRAMEBUFFER) + @assert status == GL_FRAMEBUFFER_COMPLETE + + + # Second Framebuffer + # postprocessor adds buffers here + color_luma_framebuffer = glGenFramebuffers() + glBindFramebuffer(GL_FRAMEBUFFER, color_luma_framebuffer) + + @assert status == GL_FRAMEBUFFER_COMPLETE + + glBindFramebuffer(GL_FRAMEBUFFER, 0) + fb_size_node = Node(fb_size) + + buffers = Dict( + :color => color_buffer, + :objectid => objectid_buffer, + :depth => depth_buffer + ) + + return GLFramebuffer( + fb_size_node, + (render_framebuffer, color_luma_framebuffer), + buffers, + [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1] + ) +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 + end + nothing +end + + +struct MonitorProperties + name::String + isprimary::Bool + position::Vec{2, Int} + physicalsize::Vec{2, Int} + videomode::GLFW.VidMode + videomode_supported::Vector{GLFW.VidMode} + dpi::Vec{2, Float64} + monitor::GLFW.Monitor +end + +function MonitorProperties(monitor::GLFW.Monitor) + name = GLFW.GetMonitorName(monitor) + isprimary = GLFW.GetPrimaryMonitor() == monitor + position = Vec{2, Int}(GLFW.GetMonitorPos(monitor)...) + physicalsize = Vec{2, Int}(GLFW.GetMonitorPhysicalSize(monitor)...) + videomode = GLFW.GetVideoMode(monitor) + sfactor = Sys.isapple() ? 2.0 : 1.0 + dpi = Vec(videomode.width * 25.4, videomode.height * 25.4) * sfactor ./ Vec{2, Float64}(physicalsize) + videomode_supported = GLFW.GetVideoModes(monitor) + + MonitorProperties(name, isprimary, position, physicalsize, videomode, videomode_supported, dpi, monitor) +end + +was_destroyed(nw::GLFW.Window) = nw.handle == C_NULL + +function GLContext() + context = GLFW.GetCurrentContext() + version = opengl_version_number() + glsl_version = glsl_version_number() + return GLContext(context, version, glsl_version, unique_context_counter()) +end + +function ShaderAbstractions.native_switch_context!(x::GLFW.Window) + GLFW.MakeContextCurrent(x) +end + +function ShaderAbstractions.native_context_alive(x::GLFW.Window) + GLFW.is_initialized() && !was_destroyed(x) +end + +function destroy!(nw::GLFW.Window) + was_current = ShaderAbstractions.is_current_context(nw) + if !was_destroyed(nw) + GLFW.SetWindowShouldClose(nw, true) + GLFW.PollEvents() + GLFW.DestroyWindow(nw) + nw.handle = C_NULL + end + was_current && ShaderAbstractions.switch_context!() +end + +function windowsize(nw::GLFW.Window) + was_destroyed(nw) && return (0, 0) + size = GLFW.GetFramebufferSize(nw) + return (size.width, size.height) +end + +function Base.isopen(window::GLFW.Window) + was_destroyed(window) && return false + try + return !GLFW.WindowShouldClose(window) + catch e + # can't be open if GLFW is already terminated + e.code == GLFW.NOT_INITIALIZED && return false + rethrow(e) + end +end diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl new file mode 100644 index 00000000000..fd829ce5ffa --- /dev/null +++ b/GLMakie/src/postprocessing.jl @@ -0,0 +1,262 @@ +# Utilities: +function draw_fullscreen(vao_id) + glBindVertexArray(vao_id) + glDrawArrays(GL_TRIANGLES, 0, 3) + glBindVertexArray(0) + return +end + +struct PostprocessPrerender end + +function (sp::PostprocessPrerender)() + glDepthMask(GL_TRUE) + glDisable(GL_DEPTH_TEST) + glDisable(GL_BLEND) + glDisable(GL_CULL_FACE) + return +end + +const PostProcessROBJ = RenderObject{PostprocessPrerender} + +rcpframe(x) = 1f0 ./ Vec2f0(x[1], x[2]) + +struct PostProcessor{F} + robjs::Vector{PostProcessROBJ} + render::F +end + +function empty_postprocessor(args...; kwargs...) + PostProcessor(PostProcessROBJ[], screen -> nothing) +end + + + +function ssao_postprocessor(framebuffer) + # Add missing buffers + if !haskey(framebuffer.buffers, :position) + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) + position_buffer = Texture( + Vec4f0, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge + ) + attach_framebuffer(position_buffer, GL_COLOR_ATTACHMENT2) + push!(framebuffer.buffers, :position => position_buffer) + end + if !haskey(framebuffer.buffers, :normal) + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) + normal_occlusion_buffer = Texture( + Vec4f0, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge + ) + attach_framebuffer(normal_occlusion_buffer, GL_COLOR_ATTACHMENT3) + push!(framebuffer.buffers, :normal_occlusion => normal_occlusion_buffer) + end + + # Add buffers written in primary render (before postprocessing) + if !(GL_COLOR_ATTACHMENT2 in framebuffer.render_buffer_ids) + push!(framebuffer.render_buffer_ids, GL_COLOR_ATTACHMENT2) + end + if !(GL_COLOR_ATTACHMENT3 in framebuffer.render_buffer_ids) + push!(framebuffer.render_buffer_ids, GL_COLOR_ATTACHMENT3) + end + + # SSAO setup + N_samples = 64 + lerp_min = 0.1f0 + lerp_max = 1.0f0 + kernel = map(1:N_samples) do i + n = normalize([2.0rand() .- 1.0, 2.0rand() .- 1.0, rand()]) + scale = lerp_min + (lerp_max - lerp_min) * (i / N_samples)^2 + v = Vec3f0(scale * rand() * n) + end + + + + # compute occlusion + shader1 = LazyShader( + loadshader("postprocessing/fullscreen.vert"), + loadshader("postprocessing/SSAO.frag"), + view = Dict( + "N_samples" => "$N_samples" + ) + ) + data1 = Dict{Symbol, Any}( + :position_buffer => framebuffer.buffers[:position], + :normal_occlusion_buffer => framebuffer.buffers[:normal_occlusion], + :kernel => kernel, + :noise => Texture( + [normalize(Vec2f0(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], + minfilter = :nearest, x_repeat = :repeat + ), + :noise_scale => map(s -> Vec2f0(s ./ 4.0), framebuffer.resolution), + :projection => Node(Mat4f0(I)), + :bias => Node(0.025f0), + :radius => Node(0.5f0) + ) + pass1 = RenderObject(data1, shader1, PostprocessPrerender(), nothing) + pass1.postrenderfunction = () -> draw_fullscreen(pass1.vertexarray.id) + + + # blur occlusion and combine with color + shader2 = LazyShader( + loadshader("postprocessing/fullscreen.vert"), + loadshader("postprocessing/SSAO_blur.frag") + ) + data2 = Dict{Symbol, Any}( + :normal_occlusion => framebuffer.buffers[:normal_occlusion], + :color_texture => framebuffer.buffers[:color], + :ids => framebuffer.buffers[:objectid], + :inv_texel_size => lift(rcpframe, framebuffer.resolution), + :blur_range => Node(Int32(2)) + ) + pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing) + pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) + + + + full_render = screen -> begin + fb = screen.framebuffer + w, h = size(fb) + + # Setup rendering + # SSAO - calculate occlusion + glDrawBuffer(GL_COLOR_ATTACHMENT3) # occlusion buffer + glViewport(0, 0, w, h) + # glClearColor(1, 1, 1, 1) # 1 means no darkening + # glClear(GL_COLOR_BUFFER_BIT) + glDisable(GL_STENCIL_TEST) + glEnable(GL_SCISSOR_TEST) + + for (screenid, scene) in screen.screens + # Select the area of one leaf scene + # This should be per scene because projection may vary between + # 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)...) + # update uniforms + SSAO = scene.SSAO + data1[:projection][] = scene.camera.projection[] + data1[:bias][] = Float32(to_value(get(SSAO, :bias, 0.025))) + data1[:radius][] = Float32(to_value(get(SSAO, :radius, 0.5))) + GLAbstraction.render(pass1) + end + + + # SSAO - blur occlusion and apply to color + glDrawBuffer(GL_COLOR_ATTACHMENT0) # color buffer + 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)...) + # update uniforms + SSAO = scene.attributes.SSAO + data2[:blur_range][] = Int32(to_value(get(SSAO, :blur, 2))) + GLAbstraction.render(pass2) + end + glDisable(GL_SCISSOR_TEST) + end + + PostProcessor([pass1, pass2], full_render) +end + + + +""" + fxaa_postprocessor(framebuffer) + +Returns a PostProcessor that handles fxaa. +""" +function fxaa_postprocessor(framebuffer) + # Add missing buffers + if !haskey(framebuffer.buffers, :color_luma) + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[2]) + color_luma_buffer = Texture( + RGBA{N0f8}, size(framebuffer), minfilter=:linear, x_repeat=:clamp_to_edge + ) + attach_framebuffer(color_luma_buffer, GL_COLOR_ATTACHMENT0) + push!(framebuffer.buffers, :color_luma => color_luma_buffer) + end + + + + # calculate luma for FXAA + shader1 = LazyShader( + loadshader("postprocessing/fullscreen.vert"), + loadshader("postprocessing/postprocess.frag") + ) + data1 = Dict{Symbol, Any}( + :color_texture => framebuffer.buffers[:color] + ) + pass1 = RenderObject(data1, shader1, PostprocessPrerender(), nothing) + pass1.postrenderfunction = () -> draw_fullscreen(pass1.vertexarray.id) + + # perform FXAA + shader2 = LazyShader( + loadshader("postprocessing/fullscreen.vert"), + loadshader("postprocessing/fxaa.frag") + ) + data2 = Dict{Symbol, Any}( + :color_texture => framebuffer.buffers[:color_luma], + :RCPFrame => lift(rcpframe, framebuffer.resolution), + ) + pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing) + pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) + + + + full_render = screen -> begin + fb = screen.framebuffer + w, h = size(fb) + + # FXAA - calculate LUMA + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[2]) + glDrawBuffer(GL_COLOR_ATTACHMENT0) # color_luma buffer + glViewport(0, 0, w, h) + # necessary with negative SSAO bias... + glClearColor(1, 1, 1, 1) + glClear(GL_COLOR_BUFFER_BIT) + GLAbstraction.render(pass1) + + # FXAA - perform anti-aliasing + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) + glDrawBuffer(GL_COLOR_ATTACHMENT0) # color buffer + # glViewport(0, 0, w, h) # not necessary + GLAbstraction.render(pass2) + end + + PostProcessor([pass1, pass2], full_render) +end + + +""" + to_screen_postprocessor(framebuffer) + +Sets up a Postprocessor which copies the color buffer to the screen. Used as a +final step for displaying the screen. +""" +function to_screen_postprocessor(framebuffer) + # draw color buffer + shader = LazyShader( + loadshader("postprocessing/fullscreen.vert"), + loadshader("postprocessing/copy.frag") + ) + data = Dict{Symbol, Any}( + :color_texture => framebuffer.buffers[:color] + ) + pass = RenderObject(data, shader, PostprocessPrerender(), nothing) + pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) + + full_render = screen -> begin + fb = screen.framebuffer + w, h = size(fb) + + # transfer everything to the screen + glBindFramebuffer(GL_FRAMEBUFFER, 0) + glViewport(0, 0, w, h) + glClear(GL_COLOR_BUFFER_BIT) + GLAbstraction.render(pass) # copy postprocess + end + + PostProcessor([pass], full_render) +end diff --git a/GLMakie/src/precompile.jl b/GLMakie/src/precompile.jl new file mode 100644 index 00000000000..4f69014e4ce --- /dev/null +++ b/GLMakie/src/precompile.jl @@ -0,0 +1,30 @@ +function _precompile_() + ccall(:jl_generating_output, Cint, ()) == 1 || return nothing + precompile(Makie.backend_display, (GLBackend, Scene)) + + # These are awful and will go stale as gensyms change (check by putting `@assert` in front of each one). + # It would be far better to fix the inference problems. + isdefined(GLMakie, Symbol("#89#90")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#89#90")),Text{Tuple{String}}}) # time: 1.7054044 + isdefined(GLMakie, Symbol("#72#78")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#72#78")),SMatrix{4, 4, Float32, 16},SMatrix{4, 4, Float32, 16}}) # time: 0.18958463 + isdefined(GLMakie, Symbol("#44#46")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#44#46")),GLFW.Window}) # time: 0.098638594 + isdefined(GLMakie, Symbol("#81#82")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#81#82")),Symbol}) # time: 0.07212781 + Base.precompile(Tuple{typeof(draw_atomic),Screen,Scene,Union{Scatter{ArgType} where ArgType, MeshScatter{ArgType} where ArgType}}) # time: 0.06695828 + isdefined(GLMakie, Symbol("#89#90")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#89#90")),LineSegments{Tuple{Vector{Point{2, Float32}}}}}) # time: 0.050924074 + isdefined(GLMakie, Symbol("#104#109")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#104#109")),String,Vector{Point{3, Float32}},Vector{Float32},Vector{FTFont},Vec{2, Float32},Vector{Quaternionf0},SMatrix{4, 4, Float32, 16},Float64,Float64}) # time: 0.038516257 + isdefined(GLMakie, Symbol("#81#82")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#81#82")),Bool}) # time: 0.030008739 + isdefined(GLMakie, Symbol("#101#102")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#101#102")),Int64,Point{3, Float32},Float32,FTFont,Vec{2, Float32}}) # time: 0.029477166 + isdefined(GLMakie, Symbol("#81#82")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#81#82")),Symbol}) # time: 0.019217245 + isdefined(GLMakie, Symbol("#81#82")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#81#82")),Billboard}) # time: 0.016408404 + isdefined(GLMakie, Symbol("#81#82")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#81#82")),Type}) # time: 0.006553374 + Base.precompile(Tuple{typeof(renderloop),Screen}) # time: 0.005448615 + isdefined(GLMakie, Symbol("#81#82")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#81#82")),RGBA{N0f8}}) # time: 0.004190753 + Base.precompile(Tuple{typeof(push!),Screen,Scene,RenderObject{GLMakie.GLAbstraction.StandardPrerender}}) # time: 0.002534885 + isdefined(GLMakie, Symbol("#81#82")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#81#82")),Any}) # time: 0.002144091 + isdefined(GLMakie, Symbol("#81#82")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#81#82")),Any}) # time: 0.001724354 + isdefined(GLMakie, Symbol("#12#20")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#12#20"))}) # time: 0.00144086 + isdefined(GLMakie, Symbol("#81#82")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#81#82")),Vector{RGBA{Float32}}}) # time: 0.001202468 + Base.precompile(Tuple{typeof(draw_atomic),Screen,Scene,Lines{ArgType} where ArgType}) # time: 0.001200726 + isdefined(GLMakie, Symbol("#81#82")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#81#82")),Tuple{Symbol, Float64}}) # time: 0.001081687 + isdefined(GLMakie, Symbol("#81#82")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#81#82")),Vector{Float32}}) # time: 0.001065034 + isdefined(GLMakie, Symbol("#89#90")) && Base.precompile(Tuple{getfield(GLMakie, Symbol("#89#90")),Annotations{Tuple{Vector{Tuple{String, Point{2, Float32}}}}}}) # time: 0.001031033 +end diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl new file mode 100644 index 00000000000..13758193bd1 --- /dev/null +++ b/GLMakie/src/rendering.jl @@ -0,0 +1,223 @@ +# TODO process!(scene, RenderTickEvent()) +function vsynced_renderloop(screen) + while isopen(screen) && !WINDOW_CONFIG.exit_renderloop[] + pollevents(screen) # GLFW poll + screen.render_tick[] = nothing + if WINDOW_CONFIG.pause_rendering[] + sleep(0.1) + else + make_context_current(screen) + render_frame(screen) + GLFW.SwapBuffers(to_native(screen)) + yield() + end + end +end + +function fps_renderloop(screen::Screen, framerate=WINDOW_CONFIG.framerate[]) + time_per_frame = 1.0 / framerate + while isopen(screen) && !WINDOW_CONFIG.exit_renderloop[] + t = time_ns() + pollevents(screen) # GLFW poll + screen.render_tick[] = nothing + if WINDOW_CONFIG.pause_rendering[] + sleep(0.1) + else + make_context_current(screen) + render_frame(screen) + GLFW.SwapBuffers(to_native(screen)) + t_elapsed = (time_ns() - t) / 1e9 + diff = time_per_frame - t_elapsed + if diff > 0.001 # can't sleep less than 0.001 + sleep(diff) + else # if we don't sleep, we still need to yield explicitely to other tasks + yield() + end + end + end +end + +function renderloop(screen; framerate=WINDOW_CONFIG.framerate[]) + isopen(screen) || error("Screen most be open to run renderloop!") + try + if WINDOW_CONFIG.vsync[] + GLFW.SwapInterval(1) + vsynced_renderloop(screen) + else + GLFW.SwapInterval(0) + fps_renderloop(screen, framerate) + end + catch e + showerror(stderr, e, catch_backtrace()) + println(stderr) + rethrow(e) + finally + destroy!(screen) + end +end + +const WINDOW_CONFIG = (renderloop = Ref{Function}(renderloop), + vsync = Ref(false), + framerate = Ref(30.0), + float = Ref(false), + pause_rendering = Ref(false), + focus_on_show = Ref(false), + decorated = Ref(true), + title = Ref("Makie"), + exit_renderloop = Ref(false),) + +""" + set_window_config!(; + renderloop = renderloop, + vsync = false, + framerate = 30.0, + float = false, + pause_rendering = false, + focus_on_show = false, + decorated = true, + title = "Makie" + ) +Updates the screen configuration, will only go into effect after closing the current +window and opening a new one! +""" +function set_window_config!(; kw...) + for (key, value) in kw + getfield(WINDOW_CONFIG, key)[] = value + end +end + +function setup!(screen) + glEnable(GL_SCISSOR_TEST) + if isopen(screen) + glScissor(0, 0, widths(screen)...) + 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)...) + glViewport(rt...) + bits = GL_STENCIL_BUFFER_BIT + glClearStencil(id) + if scene.clear + c = to_color(scene.backgroundcolor[]) + glScissor(rt...) + glClearColor(red(c), green(c), blue(c), alpha(c)) + bits |= GL_COLOR_BUFFER_BIT + glClear(bits) + end + end + end + end + glDisable(GL_SCISSOR_TEST) + return +end + +const selection_queries = Function[] + +""" +Renders a single frame of a `window` +""" +function render_frame(screen::Screen; resize_buffers=true) + nw = to_native(screen) + ShaderAbstractions.is_context_active(nw) || return + fb = screen.framebuffer + if resize_buffers + wh = Int.(framebuffer_size(nw)) + resize!(fb, wh) + end + w, h = size(fb) + + # prepare stencil (for sub-scenes) + glEnable(GL_STENCIL_TEST) + glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) # color framebuffer + glDrawBuffers(length(fb.render_buffer_ids), fb.render_buffer_ids) + glEnable(GL_STENCIL_TEST) + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE) + glStencilMask(0xff) + glClearStencil(0) + glClearColor(0, 0, 0, 0) + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT) + glDrawBuffer(fb.render_buffer_ids[1]) + setup!(screen) + glDrawBuffers(length(fb.render_buffer_ids), fb.render_buffer_ids) + + # render with FXAA & SSAO + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE) + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE) + glStencilMask(0x00) + GLAbstraction.render(screen, true, true) + + + # SSAO + screen.postprocessors[1].render(screen) + + # render with FXAA but no SSAO + glDrawBuffers(2, [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1]) + glEnable(GL_STENCIL_TEST) + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE) + glStencilMask(0x00) + GLAbstraction.render(screen, true, false) + glDisable(GL_STENCIL_TEST) + + # FXAA + screen.postprocessors[2].render(screen) + + + # no FXAA primary render + glDrawBuffers(2, [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1]) + glEnable(GL_STENCIL_TEST) + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE) + glStencilMask(0x00) + GLAbstraction.render(screen, false) + glDisable(GL_STENCIL_TEST) + + # transfer everything to the screen + screen.postprocessors[3].render(screen) + + + return +end + +function id2scene(screen, id1) + # TODO maybe we should use a different data structure + for (id2, scene) in screen.screens + id1 == id2 && return true, scene + end + return false, nothing +end + +function GLAbstraction.render(screen::GLScreen, fxaa::Bool, ssao::Bool=false) + # Somehow errors in here get ignored silently!? + try + # sort by overdraw, so that overdrawing objects get drawn last! + # sort!(screen.renderlist, by = ((zi, id, robj),)-> robj.prerenderfunction.overdraw[]) + for (zindex, screenid, elem) in screen.renderlist + found, scene = id2scene(screen, screenid) + found || continue + a = pixelarea(scene)[] + glViewport(minimum(a)..., widths(a)...) + if scene.clear + glStencilFunc(GL_EQUAL, screenid, 0xff) + else + # if we don't clear, that means we have a screen that is overlaid + # on top of another, which means it doesn't have a stencil value + # so we can't do the stencil test + glStencilFunc(GL_ALWAYS, screenid, 0xff) + end + if (fxaa && elem[:fxaa][]) && ssao && elem[:ssao][] + render(elem) + end + if (fxaa && elem[:fxaa][]) && !ssao && !elem[:ssao][] + render(elem) + end + if !fxaa && !elem[:fxaa][] + render(elem) + end + end + catch e + @error "Error while rendering!" exception = e + rethrow(e) + end + return +end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl new file mode 100644 index 00000000000..b857489012e --- /dev/null +++ b/GLMakie/src/screen.jl @@ -0,0 +1,548 @@ +const ScreenID = UInt8 +const ZIndex = Int +# ID, Area, clear, is visible, background color +const ScreenArea = Tuple{ScreenID, Scene} + +abstract type GLScreen <: AbstractScreen end + +mutable struct Screen <: GLScreen + glscreen::GLFW.Window + framebuffer::GLFramebuffer + rendertask::RefValue{Task} + screen2scene::Dict{WeakRef, ScreenID} + screens::Vector{ScreenArea} + renderlist::Vector{Tuple{ZIndex, ScreenID, RenderObject}} + postprocessors::Vector{PostProcessor} + cache::Dict{UInt64, RenderObject} + cache2plot::Dict{UInt16, AbstractPlot} + framecache::Matrix{RGB{N0f8}} + render_tick::Observable{Nothing} + window_open::Observable{Bool} + function Screen( + glscreen::GLFW.Window, + framebuffer::GLFramebuffer, + rendertask::RefValue{Task}, + screen2scene::Dict{WeakRef, ScreenID}, + screens::Vector{ScreenArea}, + renderlist::Vector{Tuple{ZIndex, ScreenID, RenderObject}}, + postprocessors::Vector{PostProcessor}, + cache::Dict{UInt64, RenderObject}, + cache2plot::Dict{UInt16, AbstractPlot}, + ) + s = size(framebuffer) + obj = new( + glscreen, framebuffer, rendertask, screen2scene, + screens, renderlist, postprocessors, cache, cache2plot, + Matrix{RGB{N0f8}}(undef, s), Observable(nothing), + Observable(true) + ) + end +end + +GeometryBasics.widths(x::Screen) = size(x.framebuffer) + +Base.wait(x::Screen) = isassigned(x.rendertask) && wait(x.rendertask[]) +Base.wait(scene::Scene) = wait(Makie.getscreen(scene)) +Base.show(io::IO, screen::Screen) = print(io, "GLMakie.Screen(...)") +Base.size(x::Screen) = size(x.framebuffer) + +function insertplots!(screen::GLScreen, scene::Scene) + get!(screen.screen2scene, WeakRef(scene)) do + id = length(screen.screens) + 1 + push!(screen.screens, (id, scene)) + return id + end + for elem in scene.plots + insert!(screen, scene, elem) + end + foreach(s-> insertplots!(screen, s), scene.children) +end + +function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot) + if !isempty(plot.plots) + # this plot consists of children, so we flatten it and delete the children instead + delete!.(Ref(screen), Ref(scene), Makie.flatten_plots(plot)) + else + renderobject = get(screen.cache, objectid(plot)) do + error("Could not find $(typeof(subplot)) in current GLMakie screen!") + end + + # These need explicit clean up because (some of) the source nodes + # remain whe the plot is deleated. + for k in (:lightposition, :normalmatrix) + if haskey(renderobject.uniforms, k) + n = renderobject.uniforms[k] + for input in n.inputs + off(input) + end + end + end + + filter!(x-> x[3] !== renderobject, screen.renderlist) + end +end + +function Base.empty!(screen::Screen) + empty!(screen.renderlist) + empty!(screen.screen2scene) + empty!(screen.screens) +end + +function destroy!(screen::Screen) + empty!(screen) + screen.window_open[] = false + empty!(screen.cache) + empty!(screen.cache2plot) + destroy!(screen.glscreen) +end + +Base.close(screen::Screen) = destroy!(screen) + +function resize_native!(window::GLFW.Window, resolution...; wait_for_resize=true) + if isopen(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)) + # We don't wait for the window to be resized + wait_for_resize || return + # There is a problem, that window size update seems to take an arbitrary + # amount of time - GLFW.WaitEvents() / a single GLFW.PollEvent() + # doesn't help, so we try it a couple of times, to make sure + # we have the desired size in the end + for i in 1:100 + isopen(window) || return + newsize = windowsize(window) + # we aren't guaranteed to get exactly w & h, since the window + # manager is allowed to restrict the size... + # So we can only test, if the size changed, but not if it matches + # the desired size! + newsize != oldsize && return + # There is a bug here, were without `sleep` it doesn't update the size + # Not sure who's fault it is, but PollEvents/yield both dont work - only sleep! + GLFW.PollEvents() + sleep(0.0001) + end + 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)) +end + +function fast_color_data!(dest::Array{RGB{N0f8}, 2}, source::Texture{T, 2}) where T + GLAbstraction.bind(source) + glPixelStorei(GL_PACK_ALIGNMENT, 1) + glGetTexImage(source.texturetype, 0, GL_RGB, GL_UNSIGNED_BYTE, dest) + GLAbstraction.bind(source, 0) + nothing +end + +""" +depthbuffer(screen::Screen) +Gets the depth buffer of screen. +Usage: +``` +using Makie, GLMakie +x = scatter(1:4) +screen = display(x) +depth_color = GLMakie.depthbuffer(screen) +# Look at result: +heatmap(depth_color, colormap=:grays, show_axis=false) +``` +""" +function depthbuffer(screen::Screen) + render_frame(screen, resize_buffers=false) # let it render + glFinish() # block until opengl is done rendering + source = screen.framebuffer.buffers[:depth] + depth = Matrix{Float32}(undef, size(source)) + GLAbstraction.bind(source) + GLAbstraction.glGetTexImage(source.texturetype, 0, GL_DEPTH_COMPONENT, GL_FLOAT, depth) + GLAbstraction.bind(source, 0) + return depth +end + +function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Makie.JuliaNative) + if !isopen(screen) + error("Screen not open!") + end + 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() + # keep current buffer size to allows larger-than-window renders + render_frame(screen, resize_buffers=false) # let it render + glFinish() # block until opengl is done rendering + if size(ctex) != size(screen.framecache) + screen.framecache = Matrix{RGB{N0f8}}(undef, size(ctex)) + end + fast_color_data!(screen.framecache, ctex) + if format == Makie.GLNative + return screen.framecache + elseif format == Makie.JuliaNative + @static if VERSION < v"1.6" + bufc = copy(screen.framecache) + ind1, ind2 = axes(bufc) + n = first(ind2) + last(ind2) + for i in ind1 + @simd for j in ind2 + @inbounds bufc[i, n-j] = screen.framecache[i, j] + end + end + screen.framecache = bufc + else + reverse!(screen.framecache, dims = 2) + end + return PermutedDimsArray(screen.framecache, (2,1)) + end +end + + +Base.isopen(x::Screen) = isopen(x.glscreen) +function Base.push!(screen::GLScreen, scene::Scene, robj) + # filter out gc'ed elements + filter!(screen.screen2scene) do (k, v) + k.value !== nothing + end + screenid = get!(screen.screen2scene, WeakRef(scene)) do + id = length(screen.screens) + 1 + push!(screen.screens, (id, scene)) + return id + end + push!(screen.renderlist, (0, screenid, robj)) + return robj +end + +to_native(x::Screen) = x.glscreen + +""" +OpenGL shares all data containers between shared contexts, but not vertexarrays -.- +So to share a robjs between a context, we need to rewrap the vertexarray into a new one for that +specific context. +""" +function rewrap(robj::RenderObject{Pre}) where Pre + RenderObject{Pre}( + robj.main, + robj.uniforms, + GLVertexArray(robj.vertexarray), + robj.prerenderfunction, + robj.postrenderfunction, + robj.boundingbox, + ) +end + +const GLOBAL_GL_SCREEN = Ref{Screen}() +const gl_screens = GLFW.Window[] + +function global_gl_screen() + screen = if isassigned(GLOBAL_GL_SCREEN) && isopen(GLOBAL_GL_SCREEN[]) + GLOBAL_GL_SCREEN[] + else + GLOBAL_GL_SCREEN[] = Screen() + GLOBAL_GL_SCREEN[] + end + return screen +end + +""" +Loads the makie loading icon and embedds it in an image the size of resolution +""" +function get_loading_image(resolution) + icon = Matrix{N0f8}(undef, 192, 192) + open(joinpath(@__DIR__, "..", "assets", "loading.bin")) do io + read!(io, icon) + end + img = zeros(RGBA{N0f8}, resolution...) + center = resolution .÷ 2 + center_icon = size(icon) .÷ 2 + start = CartesianIndex(max.(center .- center_icon, 1)) + I1 = CartesianIndex(1, 1) + stop = min(start + CartesianIndex(size(icon)) - I1, CartesianIndex(resolution)) + for idx in start:stop + gray = icon[idx - start + I1] + img[idx] = RGBA{N0f8}(gray, gray, gray, 1.0) + end + return img +end + +function display_loading_image(screen::Screen) + fb = screen.framebuffer + fbsize = size(fb) + image = get_loading_image(fbsize) + if size(image) == fbsize + nw = to_native(screen) + # transfer loading image to gpu framebuffer + fb.buffers[:color][1:size(image, 1), 1:size(image, 2)] = image + ShaderAbstractions.is_context_active(nw) || return + w, h = fbsize + glBindFramebuffer(GL_FRAMEBUFFER, 0) # transfer back to window + glViewport(0, 0, w, h) + glClearColor(0, 0, 0, 0) + glClear(GL_COLOR_BUFFER_BIT) + # GLAbstraction.render(fb.postprocess[end]) # copy postprocess + GLAbstraction.render(screen.postprocessors[end].robjs[1]) + GLFW.SwapBuffers(nw) + else + error("loading_image needs to be Matrix{RGBA{N0f8}} with size(loading_image) == resolution") + end +end + + +function Screen(; + resolution = (10, 10), visible = false, title = WINDOW_CONFIG.title[], + kw_args... + ) + if !isempty(gl_screens) + for elem in gl_screens + isopen(elem) && destroy!(elem) + end + empty!(gl_screens) + end + # Somehow this constant isn't wrapped by glfw + GLFW_FOCUS_ON_SHOW = 0x0002000C + windowhints = [ + (GLFW.SAMPLES, 0), + (GLFW.DEPTH_BITS, 0), + + # SETTING THE ALPHA BIT IS REALLY IMPORTANT ON OSX, SINCE IT WILL JUST KEEP SHOWING A BLACK SCREEN + # WITHOUT ANY ERROR -.- + (GLFW.ALPHA_BITS, 8), + (GLFW.RED_BITS, 8), + (GLFW.GREEN_BITS, 8), + (GLFW.BLUE_BITS, 8), + + (GLFW.STENCIL_BITS, 0), + (GLFW.AUX_BUFFERS, 0), + (GLFW_FOCUS_ON_SHOW, WINDOW_CONFIG.focus_on_show[]), + (GLFW.DECORATED, WINDOW_CONFIG.decorated[]), + (GLFW.FLOATING, WINDOW_CONFIG.float[]), + ] + + window = try + GLFW.Window( + name = title, resolution = (10, 10), # 10, because smaller sizes seem to error on some platforms + windowhints = windowhints, + visible = false, + focus = false, + kw_args... + ) + catch e + @warn(""" + GLFW couldn't create an OpenGL window. + This likely means, you don't have an OpenGL capable Graphic Card, + or you don't have an OpenGL 3.3 capable video driver installed. + Have a look at the troubleshooting section in the GLMakie readme: + https://github.com/JuliaPlots/Makie.jl/tree/master/GLMakie#troubleshooting-opengl. + """) + rethrow(e) + end + + GLFW.SetWindowIcon(window, Makie.icon()) + + # tell GLAbstraction that we created a new context. + # This is important for resource tracking, and only needed for the first context + ShaderAbstractions.switch_context!(window) + GLAbstraction.empty_shader_cache!() + push!(gl_screens, window) + + resize_native!(window, resolution...; wait_for_resize=false) + fb = GLFramebuffer(resolution) + + postprocessors = [ + enable_SSAO[] ? ssao_postprocessor(fb) : empty_postprocessor(), + enable_FXAA[] ? fxaa_postprocessor(fb) : empty_postprocessor(), + to_screen_postprocessor(fb) + ] + + screen = Screen( + window, fb, + RefValue{Task}(), + Dict{WeakRef, ScreenID}(), + ScreenArea[], + Tuple{ZIndex, ScreenID, RenderObject}[], + postprocessors, + Dict{UInt64, RenderObject}(), + Dict{UInt16, AbstractPlot}(), + ) + + GLFW.SetWindowRefreshCallback(window, window -> begin + screen.render_tick[] = nothing + render_frame(screen) + GLFW.SwapBuffers(window) + end) + + screen.rendertask[] = @async((WINDOW_CONFIG.renderloop[])(screen)) + # display window if visible! + if visible + GLFW.ShowWindow(window) + else + GLFW.HideWindow(window) + end + return screen +end + +function global_gl_screen(resolution::Tuple, visibility::Bool, tries = 1) + # ugly but easy way to find out if we create new screen. + # could just be returned by global_gl_screen, but dont want to change the API + isold = isassigned(GLOBAL_GL_SCREEN) && isopen(GLOBAL_GL_SCREEN[]) + screen = global_gl_screen() + GLFW.set_visibility!(to_native(screen), visibility) + resize!(screen, resolution...) + new_size = windowsize(to_native(screen)) + # I'm not 100% sure, if there are platforms where I'm never + # able to resize the screen (opengl might just allow that). + # so, we guard against that with just trying another resize one time! + if (new_size != resolution) && tries == 1 + # resize failed. This may happen when screen was previously + # enlarged to fill screen. WE NEED TO DESTROY!! (I think) + destroy!(screen) + # try again + return global_gl_screen(resolution, visibility, 2) + end + # show loading image on fresh screen + isold || display_loading_image(screen) + screen +end + + + +################################################################################# +### Point picking +################################################################################ + + + +function pick_native(screen::Screen, rect::IRect2D) + isopen(screen) || return Matrix{SelectionID{Int}}(undef, 0, 0) + window_size = widths(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)...) + if rx > 0 && ry > 0 && rx + rw <= w && ry + rh <= h + glReadPixels(rx, ry, rw, rh, buff.format, buff.pixeltype, sid) + for i in eachindex(sid) + if sid[i][2] > 0x3f800000 + sid[i] = SelectionID(0, sid[i].index) + end + end + return sid + else + error("Pick region $rect out of screen bounds ($w, $h).") + end +end + +function pick_native(screen::Screen, xy::Vec{2, Float64}) + isopen(screen) || return SelectionID{Int}(0, 0) + sid = Base.RefValue{SelectionID{UInt32}}() + window_size = widths(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 + if x > 0 && y > 0 && x <= w && y <= h + glReadPixels(x, y, 1, 1, buff.format, buff.pixeltype, sid) + return convert(SelectionID{Int}, sid[]) + end + return SelectionID{Int}(0, 0) +end + +function Makie.pick(scene::SceneLike, screen::Screen, xy::Vec{2, Float64}) + sid = pick_native(screen, xy) + if haskey(screen.cache2plot, sid.id) + plot = screen.cache2plot[sid.id] + return (plot, sid.index) + else + return (nothing, 0) + end +end + +function Makie.pick(scene::SceneLike, screen::Screen, rect::IRect2D) + map(pick_native(screen, rect)) do sid + if haskey(screen.cache2plot, sid.id) + (screen.cache2plot[sid.id], sid.index) + else + (nothing, sid.index) + end + end +end + + +# Skips one set of allocations +function Makie.pick_closest(scene::SceneLike, screen::Screen, xy, range) + isopen(screen) || return (nothing, 0) + w, h = widths(screen) + ((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) || return (nothing, 0) + + x0, y0 = max.(1, floor.(Int, xy .- range)) + x1, y1 = min.((w, h), floor.(Int, xy .+ range)) + dx = x1 - x0; dy = y1 - y0 + sid = pick_native(screen, IRect2D(x0, y0, dx, dy)) + + min_dist = range^2 + id = SelectionID{Int}(0, 0) + x, y = xy .+ 1 .- Vec2f0(x0, y0) + for i in 1:dx, j in 1:dy + d = (x-i)^2 + (y-j)^2 + if (d < min_dist) && (sid[i, j][1] > 0x00000000) && + (sid[i, j][2] < 0x3f800000) && haskey(screen.cache2plot, sid[i, j][1]) + min_dist = d + id = convert(SelectionID{Int}, sid[i, j]) + end + end + + if haskey(screen.cache2plot, id[1]) + return (screen.cache2plot[id[1]], id[2]) + else + return (nothing, 0) + end +end + +# Skips some allocations +function Makie.pick_sorted(scene::SceneLike, screen::Screen, xy, range) + isopen(screen) || return (nothing, 0) + w, h = widths(screen) + if !((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) + return Tuple{AbstractPlot, Int}[] + end + x0, y0 = max.(1, floor.(Int, xy .- range)) + x1, y1 = min.([w, h], floor.(Int, xy .+ range)) + dx = x1 - x0; dy = y1 - y0 + + picks = pick_native(screen, IRect2D(x0, y0, dx, dy)) + + selected = filter(x -> x[1] > 0 && haskey(screen.cache2plot, x[1]), unique(vec(picks))) + distances = [range^2 for _ in selected] + x, y = xy .+ 1 .- Vec2f0(x0, y0) + for i in 1:dx, j in 1:dy + if picks[i, j][1] > 0 + d = (x-i)^2 + (y-j)^2 + i = findfirst(isequal(picks[i, j]), selected) + if i === nothing + @warn "This shouldn't happen..." + elseif distances[i] > d + distances[i] = d + end + end + end + + idxs = sortperm(distances) + permute!(selected, idxs) + return map(id -> (screen.cache2plot[id[1]], id[2]), selected) +end + + +pollevents(::GLScreen) = nothing +pollevents(::Screen) = GLFW.PollEvents() diff --git a/GLMakie/src/surface_contours.frag b/GLMakie/src/surface_contours.frag new file mode 100644 index 00000000000..335c63ee765 --- /dev/null +++ b/GLMakie/src/surface_contours.frag @@ -0,0 +1,111 @@ +{{GLSL_VERSION}} + +in vec3 frag_vert; +in vec3 frag_uv; + +uniform sampler3D volumedata; + +uniform vec3 light_position = vec3(1.0, 1.0, 3.0); +uniform sampler1D colormap; +uniform vec2 colorrange; + +uniform vec3 eyeposition; + +uniform mat4 model; +uniform mat4 modelinv; +const float max_distance = 1.3; + +const int num_samples = 200; +const float step_size = max_distance / float(num_samples); + +float range01(float val, float from, float to) +{ + return clamp((val-from) / (to - from), 0.0, 1.0); +} + +vec3 gennormal(vec3 uvw, vec3 gradient_delta) +{ + vec3 a,b; + a.x = texture(volumedata, uvw - vec3(gradient_delta.x,0.0,0.0) ).r; + b.x = texture(volumedata, uvw + vec3(gradient_delta.x,0.0,0.0) ).r; + a.y = texture(volumedata, uvw - vec3(0.0,gradient_delta.y,0.0) ).r; + b.y = texture(volumedata, uvw + vec3(0.0,gradient_delta.y,0.0) ).r; + a.z = texture(volumedata, uvw - vec3(0.0,0.0,gradient_delta.z) ).r; + b.z = texture(volumedata, uvw + vec3(0.0,0.0,gradient_delta.z) ).r; + return normalize(a - b); +} + +vec3 blinn_phong(vec3 N, vec3 V, vec3 L, vec3 diffuse) +{ + // material properties + vec3 Ka = vec3(0.1); + vec3 Kd = vec3(1.0, 1.0, 1.0); + vec3 Ks = vec3(1.0, 1.0, 1.0); + float shininess = 50.0; + + // diffuse coefficient + 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 Ka * vec3(0.5) + + Kd * diffuse * diff_coeff + + Ks * vec3(0.3) * spec_coeff ; +} + +bool is_outside(vec3 position) +{ + return (position.x > 1.0 || position.y > 1.0 || position.z > 1.0 || position.x < 0.0 || position.y < 0.0 || position.z < 0.0); +} + +// Simple random generator found: http://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl +float rand(){ + return fract(sin(gl_FragCoord.x * 12.9898 + gl_FragCoord.y * 78.233) * 43758.5453); +} + +vec4 contours(vec3 front, vec3 dir, float stepsize) +{ + vec3 stepsize_dir = normalize(dir) * stepsize; + // The per-voxel alpha channel is specified in units of opacity/length. + // If our voxels are not isotropic, then the distance that we trace through + // depends on the direction. + vec3 pos = front; + float T = 1.0; + vec3 Lo = vec3(0.0); + int i = 0; + // add random offset to counteract sampling artifacts + pos += stepsize_dir * rand(); + for (i; i < num_samples && (!is_outside(pos) || i < 3) && T > 0.01; ++i, pos += stepsize_dir) { + float intensity = texture(volumedata, pos).x; + intensity = range01(intensity, colorrange.x, colorrange.y); + vec4 density = texture(colormap, intensity); + float opacity = density.a; + if(opacity > 0.0){ + vec3 N = gennormal(pos, vec3(stepsize)); + vec3 L = normalize(light_position - pos); + vec3 L2 = -L; + Lo += (T*opacity) * blinn_phong(N, pos, L, density.rgb); + Lo += (T*opacity) * blinn_phong(N, pos, L2, density.rgb); + T *= 1.0 - opacity; + } + } + return vec4(Lo, 1-T); +} + + +uniform uint objectid; + +void write2framebuffer(vec4 color, uvec2 id); + +void main() +{ + vec3 dir = normalize(frag_vert - eyeposition); + dir = vec3(modelinv * vec4(dir, 0)); + vec4 color = contours(frag_uv, dir, step_size); + write2framebuffer(color, uvec2(objectid, 0)); +} diff --git a/GLMakie/test/.gitignore b/GLMakie/test/.gitignore new file mode 100644 index 00000000000..43774ed6f60 --- /dev/null +++ b/GLMakie/test/.gitignore @@ -0,0 +1,7 @@ +tested_different +test_recordings +*.png +*.jpg +*.zip +tmax +prec diff --git a/GLMakie/test/Project.toml b/GLMakie/test/Project.toml new file mode 100644 index 00000000000..961ed74d3a0 --- /dev/null +++ b/GLMakie/test/Project.toml @@ -0,0 +1,4 @@ +[deps] +ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +ReferenceTests = "d37af2e0-5618-4e00-9939-d430db56ee94" diff --git a/GLMakie/test/glmakie_tests.jl b/GLMakie/test/glmakie_tests.jl new file mode 100644 index 00000000000..d3e273e2103 --- /dev/null +++ b/GLMakie/test/glmakie_tests.jl @@ -0,0 +1,92 @@ +using GLMakie.Makie: Record +using GLMakie.GLFW +using GLMakie.ModernGL +using GLMakie.ShaderAbstractions +using GLMakie.ShaderAbstractions: Sampler +using GLMakie.StaticArrays +using GLMakie.GeometryBasics +using ReferenceTests.RNG + +# A test case for wide lines and mitering at joints +@cell "Miter Joints for line rendering" begin + scene = Scene() + + r = 4 + sep = 4*r + scatter!(scene, (sep+2*r)*[-1,-1,1,1], (sep+2*r)*[-1,1,-1,1]) + + for i=-1:1 + for j=-1:1 + angle = pi/2 + pi/4*i + x = r*[-cos(angle/2),0,-cos(angle/2)] + y = r*[-sin(angle/2),0,sin(angle/2)] + + linewidth = 40 * 2.0^j + lines!(scene, x .+ sep*i, y .+ sep*j, color=RGBAf0(0,0,0,0.5), linewidth=linewidth) + lines!(scene, x .+ sep*i, y .+ sep*j, color=:red) + end + end + scene +end + +@cell "Sampler type" begin + # Directly access texture parameters: + x = Sampler(fill(to_color(:yellow), 100, 100), minfilter=:nearest) + scene = image(x, show_axis=false) + # indexing will go straight to the GPU, while only transfering the changes + st = Stepper(scene) + x[1:10, 1:50] .= to_color(:red) + Makie.step!(st) + x[1:10, end] .= to_color(:green) + Makie.step!(st) + x[end, end] = to_color(:blue) + Makie.step!(st) + st +end +# Test for resizing of TextureBuffer +@cell "Dynamically adjusting number of particles in a meshscatter" begin + + pos = Node(RNG.rand(Point3f0, 2)) + rot = Node(RNG.rand(Vec3f0, 2)) + color = Node(RNG.rand(RGBf0, 2)) + size = Node(0.1*RNG.rand(2)) + + makenew = Node(1) + on(makenew) do i + pos[] = RNG.rand(Point3f0, i) + rot[] = RNG.rand(Vec3f0, i) + color[] = RNG.rand(RGBf0, i) + size[] = 0.1*RNG.rand(i) + end + + scene = meshscatter(pos, + rotations=rot, + color=color, + markersize=size, + limits=FRect3D(Point3(0), Point3(1)) + ) + + Record(scene, [10, 5, 100, 60, 177]) do i + makenew[] = i + end +end + +@cell "Explicit frame rendering" begin + set_window_config!(renderloop=(screen) -> nothing) + function update_loop(m, buff, screen) + for i = 1:20 + GLFW.PollEvents() + buff .= RNG.rand.(Point3f0) .* 20f0 + m[1] = buff + GLMakie.render_frame(screen) + GLFW.SwapBuffers(GLMakie.to_native(screen)) + glFinish() + end + end + fig, ax, meshplot = meshscatter(RNG.rand(Point3f0, 10^4) .* 20f0) + screen = Makie.backend_display(GLMakie.GLBackend(), fig.scene) + buff = RNG.rand(Point3f0, 10^4) .* 20f0; + update_loop(meshplot, buff, screen) + set_window_config!(renderloop=GLMakie.renderloop) + fig +end diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl new file mode 100644 index 00000000000..ad430a060b8 --- /dev/null +++ b/GLMakie/test/runtests.jl @@ -0,0 +1,39 @@ +using GLMakie, Test +using GLMakie.FileIO +using GLMakie.Makie +using GLMakie.GeometryBasics +using GLMakie.GeometryBasics: origin +using ImageMagick +# run the unit test suite +include("unit_tests.jl") + +using ReferenceTests +using ReferenceTests: @cell + +# Run the Makie reference image testsuite +recorded = joinpath(@__DIR__, "recorded") +rm(recorded; force=true, recursive=true); mkdir(recorded) +ReferenceTests.record_tests(recording_dir=recorded) +ReferenceTests.reference_tests(recorded) +# Run the below, to generate a html to view all differences: +# recorded, ref_images, scores = ReferenceTests.reference_tests(recorded) +# ReferenceTests.generate_test_summary("preview.html", recorded, ref_images, scores) +# ReferenceTests.generate_test_summary("preview.html", recorded) + +# Run the GLMakie specific backend reference tests +empty!(ReferenceTests.DATABASE) +include("glmakie_tests.jl") +recorded_glmakie = joinpath(@__DIR__, "recorded_glmakie") +rm(recorded_glmakie; force=true, recursive=true); mkdir(recorded_glmakie) +ReferenceTests.record_tests(ReferenceTests.DATABASE, recording_dir=recorded_glmakie) +ref_images = ReferenceTests.download_refimages(; name="glmakie_refimages") +ReferenceTests.reference_tests(recorded_glmakie; ref_images=ref_images, difference=0.01) +# needs GITHUB_TOKEN to be defined +# First look at the generated refimages, to make sure they look ok: +# ReferenceTests.generate_test_summary("index_gl.html", recorded_glmakie) +# Then you can upload them to the latest major release tag with: +# ReferenceTests.upload_reference_images(recorded) + +# And do the same for the backend specific tests: +# ReferenceTests.generate_test_summary("index.html", recorded_glmakie) +# ReferenceTests.upload_reference_images(recorded_glmakie; name="glmakie_refimages") diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl new file mode 100644 index 00000000000..86258ecdfa9 --- /dev/null +++ b/GLMakie/test/unit_tests.jl @@ -0,0 +1,53 @@ +using GLMakie.Makie: backend_display, getscreen + +function project_sp(scene, point) + point_px = Makie.project(scene, point) + offset = Point2f0(minimum(pixelarea(scene)[])) + return point_px .+ offset +end + +@testset "unit tests" begin + @testset "Window handling" begin + Makie.inline!(false) + screen = GLMakie.global_gl_screen((100, 100), false) + @test isopen(screen) + fig, ax, splot = scatter(1:4); + screen2 = display(fig) + @test screen === screen2 + # TODO overload getscreen for figure + @test getscreen(ax.scene) === screen + close(screen) + + # assure we correctly close screen and remove it from plot + @test getscreen(ax.scene) === nothing + @test !events(ax.scene).window_open[] + @test isempty(events(ax.scene).window_open.listeners) + end + + @testset "Pick a plot element or plot elements inside a rectangle" begin + N = 100000 + fig, ax, splot = scatter(1:N, 1:N) + limits!(ax, 99990,100000, 99990,100000) + screen = display(fig) + # we don't really need the color buffer here, but this should be the best way right now to really + # force a full render to happen + GLMakie.Makie.colorbuffer(screen) + # test for pick a single data point (with idx > 65535) + point_px = project_sp(ax.scene, Point2f0(N-1,N-1)) + plot,idx = pick(ax.scene, point_px) + @test idx == N-1 + + # test for pick a rectangle of data points (also with some indices > 65535) + rect = FRect2D(99990.5,99990.5,8,8) + origin_px = project_sp(ax.scene, Point(origin(rect))) + tip_px = project_sp(ax.scene, Point(origin(rect) .+ widths(rect))) + rect_px = IRect2D(round.(origin_px), round.(tip_px .- origin_px)) + picks = unique(pick(ax.scene, rect_px)) + + # objects returned in plot_idx should be either grid lines (i.e. LineSegments) or Scatter points + @test all(pi-> pi[1] isa Union{LineSegments,Scatter, Makie.Mesh}, picks) + # scatter points should have indices equal to those in 99991:99998 + scatter_plot_idx = filter(pi -> pi[1] isa Scatter, picks) + @test Set(last.(scatter_plot_idx)) == Set(99991:99998) + end +end diff --git a/MakieCore/.gitignore b/MakieCore/.gitignore new file mode 100644 index 00000000000..3af67b1686f --- /dev/null +++ b/MakieCore/.gitignore @@ -0,0 +1,4 @@ +*.jl.*.cov +*.jl.cov +*.jl.mem +/docs/build/ diff --git a/MakieCore/LICENSE b/MakieCore/LICENSE new file mode 100644 index 00000000000..b00a387423d --- /dev/null +++ b/MakieCore/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Simon Danisch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MakieCore/Project.toml b/MakieCore/Project.toml new file mode 100644 index 00000000000..d85ac85e3e4 --- /dev/null +++ b/MakieCore/Project.toml @@ -0,0 +1,17 @@ +authors = ["Simon Danisch"] +name = "MakieCore" +uuid = "20f20a25-4f0e-4fdf-b5d1-57303727442b" +version = "0.1.3" + +[deps] +Observables = "510215fc-4207-5dde-b226-833fc4488ee2" + +[compat] +Observables = "0.4" +julia = "1" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test"] diff --git a/MakieCore/README.md b/MakieCore/README.md new file mode 100644 index 00000000000..74968d0065e --- /dev/null +++ b/MakieCore/README.md @@ -0,0 +1,6 @@ +# MakieCore + +[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaPlots.github.io/MakieCore.jl/stable) +[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaPlots.github.io/MakieCore.jl/dev) +[![Build Status](https://github.com/JuliaPlots/MakieCore.jl/workflows/CI/badge.svg)](https://github.com/JuliaPlots/MakieCore.jl/actions) +[![Coverage](https://codecov.io/gh/JuliaPlots/MakieCore.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaPlots/MakieCore.jl) diff --git a/MakieCore/src/MakieCore.jl b/MakieCore/src/MakieCore.jl new file mode 100644 index 00000000000..bb21ee9fbc9 --- /dev/null +++ b/MakieCore/src/MakieCore.jl @@ -0,0 +1,14 @@ +module MakieCore + +using Observables +using Observables: to_value +using Base: RefValue + + +include("types.jl") +include("attributes.jl") +include("recipes.jl") +include("basic_plots.jl") +include("conversion.jl") + +end diff --git a/src/dictlike.jl b/MakieCore/src/attributes.jl similarity index 71% rename from src/dictlike.jl rename to MakieCore/src/attributes.jl index 003a4cb32a2..044af1cd935 100644 --- a/src/dictlike.jl +++ b/MakieCore/src/attributes.jl @@ -1,31 +1,11 @@ -""" - abstract type Transformable -This is a bit of a weird name, but all scenes and plots are transformable, -so that's what they all have in common. This might be better expressed as traits. -""" -abstract type Transformable end - -abstract type AbstractPlot{Typ} <: Transformable end -abstract type AbstractScene <: Transformable end -abstract type ScenePlot{Typ} <: AbstractPlot{Typ} end -abstract type AbstractScreen <: AbstractDisplay end - -const SceneLike = Union{AbstractScene, ScenePlot} - -""" -Main structure for holding attributes, for theming plots etc! -Will turn all values into nodes, so that they can be updated. -""" -struct Attributes - attributes::Dict{Symbol, Node} -end + const Theme = Attributes Base.broadcastable(x::AbstractScene) = Ref(x) Base.broadcastable(x::AbstractPlot) = Ref(x) Base.broadcastable(x::Attributes) = Ref(x) -# The rules that we use to convert values to a Node in Attributes +# The rules that we use to convert values to a Observable in Attributes value_convert(x::Observables.AbstractObservable) = Observables.observe(x) value_convert(@nospecialize(x)) = x @@ -38,16 +18,16 @@ end value_convert(x::NamedTuple) = Attributes(x) -# Version of `convert(Node{Any}, obj)` that doesn't require runtime dispatch -node_any(@nospecialize(obj)) = isa(obj, Node{Any}) ? obj : - isa(obj, Node) ? convert(Node{Any}, obj) : Node{Any}(obj) +# Version of `convert(Observable{Any}, obj)` that doesn't require runtime dispatch +node_any(@nospecialize(obj)) = isa(obj, Observable{Any}) ? obj : + isa(obj, Observable) ? convert(Observable{Any}, obj) : Observable{Any}(obj) node_pairs(pair::Union{Pair, Tuple{Any, Any}}) = (pair[1] => node_any(value_convert(pair[2]))) node_pairs(pairs) = (node_pairs(pair) for pair in pairs) -Attributes(; kw_args...) = Attributes(Dict{Symbol, Node}(node_pairs(kw_args))) -Attributes(pairs::Pair...) = Attributes(Dict{Symbol, Node}(node_pairs(pairs))) -Attributes(pairs::AbstractVector) = Attributes(Dict{Symbol, Node}(node_pairs.(pairs))) +Attributes(; kw_args...) = Attributes(Dict{Symbol, Observable}(node_pairs(kw_args))) +Attributes(pairs::Pair...) = Attributes(Dict{Symbol, Observable}(node_pairs(pairs))) +Attributes(pairs::AbstractVector) = Attributes(Dict{Symbol, Observable}(node_pairs.(pairs))) Attributes(pairs::Iterators.Pairs) = Attributes(collect(pairs)) Attributes(nt::NamedTuple) = Attributes(; nt...) attributes(x::Attributes) = getfield(x, :attributes) @@ -112,14 +92,14 @@ end end end -function getindex(x::Attributes, key::Symbol) +function Base.getindex(x::Attributes, key::Symbol) x = attributes(x)[key] # We unpack Attributes, even though, for consistency, we store them as nodes # this makes it easier to create nested attributes return x[] isa Attributes ? x[] : x end -function setindex!(x::Attributes, value, key::Symbol) +function Base.setindex!(x::Attributes, value, key::Symbol) if haskey(x, key) x.attributes[key][] = value else @@ -127,11 +107,11 @@ function setindex!(x::Attributes, value, key::Symbol) end end -function setindex!(x::Attributes, value::Node, key::Symbol) +function Base.setindex!(x::Attributes, value::Observable, key::Symbol) if haskey(x, key) - # error("You're trying to update an attribute node with a new node. This is not supported right now. + # error("You're trying to update an attribute Observable with a new Observable. This is not supported right now. # You can do this manually like this: - # lift(val-> attributes[$key] = val, node::$(typeof(value))) + # lift(val-> attributes[$key] = val, Observable::$(typeof(value))) # ") return x.attributes[key] = node_any(value) else @@ -173,17 +153,15 @@ function Base.show(io::IO,::MIME"text/plain", attr::Attributes) end Base.show(io::IO, attr::Attributes) = show(io, MIME"text/plain"(), attr) - - theme(x::AbstractPlot) = x.attributes isvisible(x) = haskey(x, :visible) && to_value(x[:visible]) #dict interface const AttributeOrPlot = Union{AbstractPlot, Attributes} Base.pop!(x::AttributeOrPlot, args...) = pop!(x.attributes, args...) -haskey(x::AttributeOrPlot, key) = haskey(x.attributes, key) -delete!(x::AttributeOrPlot, key) = delete!(x.attributes, key) -function get!(f::Function, x::AttributeOrPlot, key::Symbol) +Base.haskey(x::AttributeOrPlot, key) = haskey(x.attributes, key) +Base.delete!(x::AttributeOrPlot, key) = delete!(x.attributes, key) +function Base.get!(f::Function, x::AttributeOrPlot, key::Symbol) if haskey(x, key) return x[key] else @@ -192,9 +170,9 @@ function get!(f::Function, x::AttributeOrPlot, key::Symbol) return x[key] end end -get!(x::AttributeOrPlot, key::Symbol, default) = get!(()-> default, x, key) -get(f::Function, x::AttributeOrPlot, key::Symbol) = haskey(x, key) ? x[key] : f() -get(x::AttributeOrPlot, key::Symbol, default) = get(()-> default, x, key) +Base.get!(x::AttributeOrPlot, key::Symbol, default) = get!(()-> default, x, key) +Base.get(f::Function, x::AttributeOrPlot, key::Symbol) = haskey(x, key) ? x[key] : f() +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" @@ -202,53 +180,53 @@ get(x::AttributeOrPlot, key::Symbol, default) = get(()-> default, x, key) # 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 # the plot itself. -getindex(plot::AbstractPlot, idx::Integer) = plot.converted[idx] -getindex(plot::AbstractPlot, idx::UnitRange{<:Integer}) = plot.converted[idx] -setindex!(plot::AbstractPlot, value, idx::Integer) = (plot.input_args[idx][] = value) +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.length(plot::AbstractPlot) = length(plot.converted) -function getindex(x::AbstractPlot, key::Symbol) +function Base.getindex(x::AbstractPlot, key::Symbol) argnames = argument_names(typeof(x), length(x.converted)) idx = findfirst(isequal(key), argnames) - if idx == nothing + if idx === nothing return x.attributes[key] else x.converted[idx] end end -function getindex(x::AttributeOrPlot, key::Symbol, key2::Symbol, rest::Symbol...) +function Base.getindex(x::AttributeOrPlot, key::Symbol, key2::Symbol, rest::Symbol...) dict = to_value(x[key]) dict isa Attributes || error("Trying to access $(typeof(dict)) with multiple keys: $key, $key2, $(rest)") dict[key2, rest...] end -function setindex!(x::AttributeOrPlot, value, key::Symbol, key2::Symbol, rest::Symbol...) +function Base.setindex!(x::AttributeOrPlot, value, key::Symbol, key2::Symbol, rest::Symbol...) dict = to_value(x[key]) dict isa Attributes || error("Trying to access $(typeof(dict)) with multiple keys: $key, $key2, $(rest)") dict[key2, rest...] = value end -function setindex!(x::AbstractPlot, value, key::Symbol) +function Base.setindex!(x::AbstractPlot, value, key::Symbol) argnames = argument_names(typeof(x), length(x.converted)) idx = findfirst(isequal(key), argnames) - if idx == nothing && haskey(x.attributes, key) + if idx === nothing && haskey(x.attributes, key) return x.attributes[key][] = value elseif !haskey(x.attributes, key) - x.attributes[key] = convert(Node, value) + x.attributes[key] = convert(Observable, value) else return setindex!(x.converted[idx], value) end end -function setindex!(x::AbstractPlot, value::Node, key::Symbol) +function Base.setindex!(x::AbstractPlot, value::Observable, key::Symbol) argnames = argument_names(typeof(x), length(x.converted)) idx = findfirst(isequal(key), argnames) - if idx == nothing + if idx === nothing if haskey(x, key) - # error("You're trying to update an attribute node with a new node. This is not supported right now. + # error("You're trying to update an attribute Observable with a new Observable. This is not supported right now. # You can do this manually like this: - # lift(val-> attributes[$key] = val, node::$(typeof(value))) + # lift(val-> attributes[$key] = val, Observable::$(typeof(value))) # ") return x.attributes[key] = value else diff --git a/MakieCore/src/basic_plots.jl b/MakieCore/src/basic_plots.jl new file mode 100644 index 00000000000..0b799386c9b --- /dev/null +++ b/MakieCore/src/basic_plots.jl @@ -0,0 +1,247 @@ + +""" + `calculated_attributes!(trait::Type{<: AbstractPlot}, plot)` +trait version of calculated_attributes +""" +calculated_attributes!(trait, plot) = nothing + +""" + `calculated_attributes!(plot::AbstractPlot)` +Fill in values that can only be calculated when we have all other attributes filled +""" +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). + +""" +@recipe(Image, x, y, image) do scene + Attributes(; + default_theme(scene)..., + colormap = [:black, :white], + colorrange = automatic, + interpolate = true, + fxaa = false, + lowclip = nothing, + highclip = nothing, + inspectable = theme(scene, :inspectable) + ) +end + +""" + heatmap(x, y, values) + heatmap(values) + +Plots a heatmap as an image on `x, y` (defaults to interpretation as dimensions). + +""" +@recipe(Heatmap, x, y, values) do scene + Attributes(; + default_theme(scene)..., + colormap = theme(scene, :colormap), + colorrange = automatic, + linewidth = 0.0, + interpolate = false, + levels = 1, + fxaa = true, + lowclip = nothing, + highclip = nothing, + inspectable = theme(scene, :inspectable) + ) +end + +""" + volume(volume_data) + +Plots a volume. Available algorithms are: +* `:iso` => IsoValue +* `:absorption` => Absorption +* `:mip` => MaximumIntensityProjection +* `:absorptionrgba` => AbsorptionRGBA +* `:additive` => AdditiveRGBA +* `:indexedabsorption` => IndexedAbsorptionRGBA +""" +@recipe(Volume, x, y, z, volume) do scene + Attributes(; + default_theme(scene)..., + algorithm = :mip, + isovalue = 0.5, + isorange = 0.05, + color = nothing, + colormap = theme(scene, :colormap), + colorrange = (0, 1), + fxaa = true, + inspectable = theme(scene, :inspectable) + ) +end + +""" + surface(x, y, 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. + +""" +@recipe(Surface, x, y, z) do scene + Attributes(; + default_theme(scene)..., + color = nothing, + colormap = theme(scene, :colormap), + colorrange = automatic, + shading = true, + fxaa = true, + lowclip = nothing, + highclip = nothing, + invert_normals = false, + inspectable = theme(scene, :inspectable) + ) +end + +""" + lines(positions) + lines(x, y) + lines(x, y, z) + +Creates a connected line plot for each element in `(x, y, z)`, `(x, y)` or `positions`. + +!!! tip + You can separate segments by inserting `NaN`s. +""" +@recipe(Lines, positions) do scene + Attributes(; + default_theme(scene)..., + linewidth = theme(scene, :linewidth), + color = theme(scene, :linecolor), + colormap = theme(scene, :colormap), + colorrange = automatic, + linestyle = nothing, + fxaa = false, + cycle = [:color], + inspectable = theme(scene, :inspectable) + ) +end + +""" + linesegments(positions) + linesegments(x, y) + linesegments(x, y, z) + +Plots a line for each pair of points in `(x, y, z)`, `(x, y)`, or `positions`. + +""" +@recipe(LineSegments, positions) do scene + default_theme(scene, Lines) +end + +# alternatively, mesh3d? Or having only mesh instead of poly + mesh and figure out 2d/3d via dispatch +""" + mesh(x, y, z) + mesh(mesh_object) + mesh(x, y, z, faces) + mesh(xyz, faces) + +Plots a 3D or 2D mesh. Supported `mesh_object`s include `Mesh` types from [GeometryBasics.jl](https://github.com/JuliaGeometry/GeometryBasics.jl). + +""" +@recipe(Mesh, mesh) do scene + Attributes(; + default_theme(scene)..., + color = :black, + colormap = theme(scene, :colormap), + colorrange = automatic, + interpolate = false, + shading = true, + fxaa = true, + inspectable = theme(scene, :inspectable), + cycle = [:color => :patchcolor], + ) +end + +""" + scatter(positions) + scatter(x, y) + scatter(x, y, z) + +Plots a marker for each element in `(x, y, z)`, `(x, y)`, or `positions`. + +""" +@recipe(Scatter, positions) do scene + Attributes(; + default_theme(scene)..., + color = theme(scene, :markercolor), + colormap = theme(scene, :colormap), + colorrange = automatic, + marker = theme(scene, :marker), + markersize = theme(scene, :markersize), + + strokecolor = theme(scene, :markerstrokecolor), + strokewidth = theme(scene, :markerstrokewidth), + glowcolor = (:black, 0.0), + glowwidth = 0.0, + + rotations = Billboard(), + marker_offset = automatic, + transform_marker = false, # Applies the plots transformation to marker + distancefield = nothing, + uv_offset_width = (0.0, 0.0, 0.0, 0.0), + markerspace = Pixel, + fxaa = false, + cycle = [:color], + inspectable = theme(scene, :inspectable) + ) +end + +""" + meshscatter(positions) + meshscatter(x, y) + meshscatter(x, y, z) + +Plots a mesh for each element in `(x, y, z)`, `(x, y)`, or `positions` (similar to `scatter`). +`markersize` is a scaling applied to the primitive passed as `marker`. + +""" +@recipe(MeshScatter, positions) do scene + Attributes(; + default_theme(scene)..., + color = :black, + colormap = theme(scene, :colormap), + colorrange = automatic, + marker = :Sphere, + markersize = 0.1, + rotations = 0.0, + # markerspace = relative, + shading = true, + fxaa = true, + inspectable = theme(scene, :inspectable), + cycle = [:color], + ) +end + +""" + text(string) + +Plots a text. + +""" +@recipe(Text, text) do scene + Attributes(; + default_theme(scene)..., + color = theme(scene, :textcolor), + font = theme(scene, :font), + strokecolor = (:black, 0.0), + strokewidth = 0, + align = (:left, :bottom), + rotation = 0.0, + textsize = 20, + position = (0.0, 0.0), + justification = automatic, + lineheight = 1.0, + space = :screen, # or :data + offset = (0.0, 0.0), + _glyphlayout = nothing, + inspectable = theme(scene, :inspectable) + ) +end diff --git a/MakieCore/src/conversion.jl b/MakieCore/src/conversion.jl new file mode 100644 index 00000000000..a3cbe0949c4 --- /dev/null +++ b/MakieCore/src/conversion.jl @@ -0,0 +1,32 @@ + +function convert_arguments end +function convert_attribute end +function used_attributes end + +################################################################################ +# Conversion Traits # +################################################################################ + +abstract type ConversionTrait end + +const XYBased = Union{MeshScatter, Scatter, Lines, LineSegments} + +struct NoConversion <: ConversionTrait end + +# No conversion by default +conversion_trait(::Type) = NoConversion() +convert_arguments(::NoConversion, args...) = args + +struct PointBased <: ConversionTrait end +conversion_trait(::Type{<: XYBased}) = PointBased() + +abstract type SurfaceLike <: ConversionTrait end + +struct ContinuousSurface <: SurfaceLike end +conversion_trait(::Type{<: Union{Surface, Image}}) = ContinuousSurface() + +struct DiscreteSurface <: SurfaceLike end +conversion_trait(::Type{<: Heatmap}) = DiscreteSurface() + +struct VolumeLike end +conversion_trait(::Type{<: Volume}) = VolumeLike() diff --git a/src/recipes.jl b/MakieCore/src/recipes.jl similarity index 65% rename from src/recipes.jl rename to MakieCore/src/recipes.jl index 486db3c2d1d..49ffa983a05 100644 --- a/src/recipes.jl +++ b/MakieCore/src/recipes.jl @@ -1,7 +1,23 @@ to_func_name(x::Symbol) = Symbol(lowercase(string(x))) # Fallback for Combined ... # Will get overloaded by recipe Macro -plotsym(::Type{Any}) = :plot +plotsym(x) = :plot + +function func2string(func::F) where F <: Function + string(F.name.mt.name) +end + +plotfunc(::Combined{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} + +plotkey(::Type{<: AbstractPlot{Typ}}) where Typ = Symbol(lowercase(func2string(Typ))) +plotkey(::T) where T <: AbstractPlot = plotkey(T) """ default_plot_signatures(funcname, funcname!, PlotType) @@ -21,6 +37,27 @@ function default_plot_signatures(funcname, funcname!, PlotType) end end +""" +Each argument can be named for a certain plot type `P`. Falls back to `arg1`, `arg2`, etc. +""" +function argument_names(plot::P) where {P<:AbstractPlot} + argument_names(P, length(plot.converted)) +end + +function argument_names(::Type{<:AbstractPlot}, num_args::Integer) + # this is called in the indexing function, so let's be a bit efficient + ntuple(i -> Symbol("arg$i"), num_args) +end + +# Since we can use Combined 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) = deepcopy(x.theme[key]) +theme(x::AbstractPlot, key) = deepcopy(x.attributes[key]) + +Attributes(x::AbstractPlot) = x.attributes + +default_theme(scene, T) = Attributes() """ # Plot Recipes in `Makie` @@ -131,14 +168,53 @@ 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))} - Makie.plotsym(::Type{<: $(PlotType)}) = $(QuoteNode(Tsym)) + const $(PlotType){$(esc(:ArgType))} = Combined{$funcname,$(esc(:ArgType))} + $(MakieCore).plotsym(::Type{<:$(PlotType)}) = $(QuoteNode(Tsym)) $(default_plot_signatures(funcname, funcname!, PlotType)) - Makie.default_theme(scene, ::Type{<: $PlotType}) = $(esc(theme_func))(scene) + $(MakieCore).default_theme(scene, ::Type{<:$PlotType}) = $(esc(theme_func))(scene) export $PlotType, $funcname, $funcname! end if !isempty(args) - push!(expr.args, :($(esc(:(Makie.argument_names)))(::Type{<: $PlotType}, len::Integer) = $args)) + push!( + expr.args, + :( + $(esc(:($(MakieCore).argument_names)))(::Type{<:$PlotType}, len::Integer) = + $args + ), + ) end 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`. +""" +function Plot(args::Vararg{Any,N}) where {N} + Combined{Any,<:Tuple{args...}} +end + +Base.@pure function Plot(::Type{T}) where {T} + Combined{Any,<:Tuple{T}} +end + +Base.@pure function Plot(::Type{T1}, ::Type{T2}) where {T1,T2} + Combined{Any,<:Tuple{T1,T2}} +end + +""" + `plottype(plot_args...)` + +Any custom argument combination that has a preferred way to be plotted should overload this. +e.g.: +```example + # make plot(rand(5, 5, 5)) plot as a volume + plottype(x::Array{<: AbstractFloat, 3}) = Volume +``` +""" +plottype(plot_args...) = Combined{Any, Tuple{typeof.(to_value.(plot_args))...}} # default to dispatch to type recipes! diff --git a/MakieCore/src/types.jl b/MakieCore/src/types.jl new file mode 100644 index 00000000000..aaccc739165 --- /dev/null +++ b/MakieCore/src/types.jl @@ -0,0 +1,83 @@ + +""" + abstract type Transformable +This is a bit of a weird name, but all scenes and plots are transformable, +so that's what they all have in common. This might be better expressed as traits. +""" +abstract type Transformable end + +abstract type AbstractPlot{Typ} <: Transformable end +abstract type AbstractScene <: Transformable end +abstract type ScenePlot{Typ} <: AbstractPlot{Typ} end +abstract type AbstractScreen <: AbstractDisplay end + +const SceneLike = Union{AbstractScene, ScenePlot} + +""" +Main structure for holding attributes, for theming plots etc! +Will turn all values into nodes, so that they can be updated. +""" +struct Attributes + attributes::Dict{Symbol, Observable} +end + +struct Combined{Typ, T} <: ScenePlot{Typ} + parent::SceneLike + transformation::Transformable + attributes::Attributes + input_args::Tuple + converted::Tuple + plots::Vector{AbstractPlot} +end + +function Base.show(io::IO, plot::Combined) + print(io, typeof(plot)) +end + +Base.parent(x::AbstractPlot) = x.parent + +struct Key{K} end +macro key_str(arg) + :(Key{$(QuoteNode(Symbol(arg)))}) +end +Base.broadcastable(x::Key) = (x,) + +""" +Type to indicate that an attribute will get calculated automatically +""" +struct Automatic end + +""" +Singleton instance to indicate that an attribute will get calculated automatically +""" +const automatic = Automatic() + +abstract type Unit{T} <: Number end + +""" +Unit in pixels on screen. +This one is a bit tricky, since it refers to a static attribute (pixels on screen don't change) +but since every visual is attached to a camera, the exact scale might change. +So in the end, this is just relative to some normed camera - the value on screen, depending on the camera, +will not actually sit on those pixels. Only camera that guarantees the correct mapping is the +`:pixel` camera type. +""" +struct Pixel{T} <: Unit{T} + value::T +end + +const px = Pixel(1) + +""" + Billboard([angle::Real]) + Billboard([angles::Vector{<: Real}]) + +Billboard attribute to always have a primitive face the camera. +Can be used for rotation. +""" +struct Billboard{T <: Union{Float32, Vector{Float32}}} + rotation::T +end +Billboard() = Billboard(0f0) +Billboard(angle::Real) = Billboard(Float32(angle)) +Billboard(angles::Vector) = Billboard(Float32.(angles)) diff --git a/MakieCore/test/runtests.jl b/MakieCore/test/runtests.jl new file mode 100644 index 00000000000..2b22c91fad6 --- /dev/null +++ b/MakieCore/test/runtests.jl @@ -0,0 +1,118 @@ +using Test, MakieCore + +# Main tests live in Makie.jl, but we should write some unit tests going forward! +using MakieCore: @recipe, Attributes, Plot +import MakieCore: plot!, convert_arguments, used_attributes, plot +import MakieCore: Observable, PointBased + +struct AbstractTimeseriesSolution + results::Vector{Float32} +end + +function plot!(plot::Plot(AbstractTimeseriesSolution)) + # plot contains any keyword arguments that you pass to plot(series; kw...) + var = get(plot, :var, Observable(5)) + density!(plot, map((v, r)-> v .* r.results, var, plot[1])) +end + +struct Test2 + series::Any +end + +struct Solution + data::Any +end + +function plot!(plot::Plot(Test2)) + arg1 = plot[1] + scatter!(plot, arg1[].series) + ser = AbstractTimeseriesSolution(arg1[].series) + sol = Solution(arg1[].series) + plot!(plot, ser, var = 10) + scatter!(plot, sol, attribute = 3, color=:red) +end + +used_attributes(::Any, x::Solution) = (:attribute,) + +# Convert for all point based types (lines, scatter) +function convert_arguments(p::MakieCore.PointBased, x::Solution; attribute = 1.0) + return convert_arguments(p, x.data .* attribute) +end + +# using GLMakie +# using CairoMakie +# plot(Test2(rand(Float32, 10))) + + +# function MakieCore.plot!(myplot::MyPlot{<:Tuple{<:AbstractVector{<:Number}}}) +# lines!(myplot, rand(10), color = myplot[:plot_color]) +# plot!(myplot, myplot[:x]) +# myplot +# end + +# myplot(1:3) + + + +# function MakieCore.plot(P::Type{<: AbstractPlot}, fig::Makie.FigurePosition, arg::Solution; axis = NamedTuple(), kwargs...) + +# menu = Menu(fig, options = ["viridis", "heat", "blues"]) + +# funcs = [sqrt, x->x^2, sin, cos] + +# menu2 = Menu(fig, options = zip(["Square Root", "Square", "Sine", "Cosine"], funcs)) + +# fig[1, 1] = vgrid!( +# Label(fig, "Colormap", width = nothing), +# menu, +# Label(fig, "Function", width = nothing), +# menu2; +# tellheight = false, width = 200) + +# ax = Axis(fig[1, 2]; axis...) + +# func = Node{Any}(funcs[1]) + +# ys = @lift($func.(arg.data)) + +# scat = plot!(ax, P, Attributes(color = ys), ys) + +# cb = Colorbar(fig[1, 3], scat) + +# on(menu.selection) do s +# scat.colormap = s +# end + +# on(menu2.selection) do s +# func[] = s +# autolimits!(ax) +# end + +# menu2.is_open = true + +# return Makie.AxisPlot(ax, scat) +# end + +# f = Figure(); +# lines(f[1, 1], Solution(0:0.3:10)) +# scatter(f[1, 2], Solution(0:0.3:10)) +# f |> display + + + +# @recipe(MyPlot, x) do scene +# Theme( +# plot_color = :red +# ) +# end + +# function Makie.plot!(p::MyPlot) +# @show p.transformation.transform_func[] +# scatter!(p, p[1]) +# end + +# myplot(rand(4), axis=(xscale=log10,)) +# using CairoMakie +# plot(Test2(rand(Float32, 10))) + +# scatter(Solution(rand(4)), attribute = 10) diff --git a/MakieRecipes/LICENSE b/MakieRecipes/LICENSE new file mode 100644 index 00000000000..fd77ff134c2 --- /dev/null +++ b/MakieRecipes/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Simon Danisch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MakieRecipes/Project.toml b/MakieRecipes/Project.toml new file mode 100644 index 00000000000..077a46609d3 --- /dev/null +++ b/MakieRecipes/Project.toml @@ -0,0 +1,22 @@ +name = "MakieRecipes" +uuid = "3c562d8e-4afa-4f1c-99bf-ad54af2b207f" +authors = ["Simon Danisch", "Anshul Singhvi "] +version = "0.1.0" + +[deps] +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +RecipesPipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c" + +[compat] +Colors = "0.9, 0.10, 0.11, 0.12" +RecipesBase = "1" +RecipesPipeline = "0.1" +julia = "1.5" + +[extras] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test", "CairoMakie"] diff --git a/MakieRecipes/README.md b/MakieRecipes/README.md new file mode 100644 index 00000000000..d5a95934126 --- /dev/null +++ b/MakieRecipes/README.md @@ -0,0 +1,9 @@ +# MakieRecipes + +[![build status](https://github.com/JuliaPlots/MakieRecipes.jl/workflows/CI/badge.svg)](https://github.com/JuliaPlots/MakieRecipes.jl/actions) +[![Docs][docs-img]][docs-url] + +[docs-img]: https://img.shields.io/badge/docs-dev-blue.svg +[docs-url]: http://juliaplots.org/MakieRecipes.jl/dev/ + +Extending Makie to support Plots.jl recipes! diff --git a/MakieRecipes/src/MakieRecipes.jl b/MakieRecipes/src/MakieRecipes.jl new file mode 100644 index 00000000000..1f98310e675 --- /dev/null +++ b/MakieRecipes/src/MakieRecipes.jl @@ -0,0 +1,47 @@ +module MakieRecipes + +# using RecipesBase, MakieCore +# using RecipesBase: @recipe +# using MakieCore: Palette, to_color + +# using RecipesPipeline +# using Colors + + +# # ## Palette +# # The default palette is defined here. + +# expand_palette(palette, n = 20; kwargs...) = RGBA.(distinguishable_colors(n, palette; kwargs...)) + +# const wong = copy(wong_colors) +# begin +# global wong +# tmp = wong[1] +# wong[1] = wong[2] +# wong[2] = tmp +# end +# const rwong = expand_palette(wong, 50) +# const default_palette = Palette(rwong) + +# include("bezier.jl") +# include("pipeline_integration.jl") +# include("attribute_table.jl") +# include("recipeplot.jl") + +# # TODO FIXME +# RecipesBase.is_key_supported(::Symbol) = false +# plots(la::MakieLayout.LAxis) = plots(la.scene) +# # FIXME TODO + +# function tomakie!(sc::AbstractScene, args...; attrs...) +# RecipesPipeline.recipe_pipeline!(sc, Dict{Symbol, Any}(attrs), args) +# end + +# tomakie!(args...; attrs...) = tomakie!(current_scene(), args...; attrs...) + +# tomakie(args...; attrs...) = tomakie!(Scene(), args...; attrs...) + + +# export tomakie, tomakie!, recipeplot, recipeplot! + +end diff --git a/MakieRecipes/src/attribute_table.jl b/MakieRecipes/src/attribute_table.jl new file mode 100644 index 00000000000..5909ceeaff0 --- /dev/null +++ b/MakieRecipes/src/attribute_table.jl @@ -0,0 +1,31 @@ +const makie_linetype = Dict{Symbol, Any}( + :auto => nothing, + :solid => nothing, + :dash => :dash, + :dot => :dot, + :dashdot => :dashdot, + :dashdotdot => [0.4, 0.2, 0.1, 0.2, 0.4] +) + +function makie_color(c) + if color === :match + return Colors.colorant"blue" + end + convert(RGBA, c) +end + +makie_seriestype_map = Dict{Symbol, Type}( + :path => Lines, + :path3d => Lines, + :scatter => Scatter, + :linesegments => LineSegments, + :heatmap => Heatmap, + :image => Image, + :spy => Spy, + :surface => Surface, + :shape => Poly, + :contour => Contour, + :curves => Bezier, + :bar => BarPlot, + # TODO: line, contour, +) diff --git a/MakieRecipes/src/bezier.jl b/MakieRecipes/src/bezier.jl new file mode 100644 index 00000000000..748dd881504 --- /dev/null +++ b/MakieRecipes/src/bezier.jl @@ -0,0 +1,90 @@ + +@recipe(Bezier) do scene + merge( + default_theme(scene, Lines), + Attributes( + npoints = 30, + colorrange = automatic + ) + ) +end + +conversion_trait(::Type{<: Bezier}) = PointBased() + +function calculated_attributes!(::Type{<: Bezier}, plot) + color_and_colormap!(plot) + pos = plot[1][] + # extend one color per linesegment to be one (the same) color per vertex + # taken from @edljk in PR #77 + if haskey(plot, :color) && isa(plot[:color][], AbstractVector) && iseven(length(pos)) && (length(pos) ÷ 2) == length(plot[:color][]) + plot[:color] = lift(plot[:color]) do cols + map(i-> cols[(i + 1) ÷ 2], 1:(length(cols) * 2)) + end + end +end + +# used in the pipeline too (for poly) +function from_nansep_vec(v::Vector{T}) where T + idxs = findall(isnan, v) + + if isempty(idxs) + return [v] + end + vs = Vector{Vector{T}}(undef, length(idxs)) + prev = 1 + num = 1 + for i in idxs + vs[num] = v[prev:i-1] + + prev = i + 1 + num += 1 + end + + return vs +end + +function bezier_value(pts::AbstractVector, t::Real) + val = 0.0 + n = length(pts) - 1 + for (i, p) in enumerate(pts) + val += p * binomial(n, i - 1) * (1 - t)^(n - i + 1) * t^(i - 1) + end + val +end + + +function to_bezier(p::Vector{Point2f0}, npoints::Int) + curves = Point2f0[] + + rawvecs = [getindex.(p, n) for n in 1:2] + + for rng in from_nansep_vec(p) + ts = LinRange(0, 1, npoints) + xs = map(t -> bezier_value(getindex.(rng, 1), t), ts) + ys = map(t -> bezier_value(getindex.(rng, 2), t), ts) + + append!(curves, Point2f0.(xs, ys)) + + push!(curves, Point2f0(NaN)) + end + + return curves +end + +function plot!(plot::Bezier) + positions = plot[1] + + @extract plot (npoints,) + + curves = lift(to_bezier, positions, npoints) + + lines!( + plot, + curves; + linestyle = plot.linestyle, + linewidth = plot.linewidth, + color = plot.color, + colormap = plot.colormap, + colorrange = plot.colorrange + ) +end diff --git a/MakieRecipes/src/layout_integration.jl b/MakieRecipes/src/layout_integration.jl new file mode 100644 index 00000000000..14bb25fce7d --- /dev/null +++ b/MakieRecipes/src/layout_integration.jl @@ -0,0 +1,4 @@ +function tomakie!(sc::AbstractScene, layout::MakieLayout.GridLayout, args...; attrs...) + # TODO create a finalizer for a Tuple{Scene, Layout, Vector{LAxis}} + RecipesPipeline.recipe_pipeline!(sc, Dict{Symbol, Any}(attrs), args) +end diff --git a/MakieRecipes/src/pipeline_integration.jl b/MakieRecipes/src/pipeline_integration.jl new file mode 100644 index 00000000000..ff43d130986 --- /dev/null +++ b/MakieRecipes/src/pipeline_integration.jl @@ -0,0 +1,391 @@ +# # RecipesPipeline API implementation + +# ## Types and aliases + +const PlotContext = Union{ + AbstractScene, + AbstractPlot, + MakieLayout.LAxis + } + +# ## API implementation + +# Define overrides for RecipesPipeline hooks. + +RecipesBase.apply_recipe(plotattributes, ::Type{T}, ::PlotContext) where T = throw(MethodError("Unmatched plot type: $T")) + +# Preprocessing involves resetting the palette for now. +# Later, it may involve setting up a layouting context, among other things. +function RecipesPipeline.preprocess_attributes!(plt::PlotContext, plotattributes) + plt.palette[].i[] = zero(UInt8) +end + + +# Allow a series type to be plotted. +RecipesPipeline.is_seriestype_supported(sc::PlotContext, st) = haskey(makie_seriestype_map, st) + +# Forward the argument preprocessing to Plots for now. +RecipesPipeline.series_defaults(sc::PlotContext, args...) = Dict{Symbol, Any}() + +# Pre-processing of user recipes +function RecipesPipeline.process_userrecipe!(sc::PlotContext, kw_list, kw) + if isa(get(kw, :marker_z, nothing), Function) + # TODO: should this take y and/or z as arguments? + kw[:marker_z] = isa(kw[:z], Nothing) ? map(kw[:marker_z], kw[:x], kw[:y]) : + map(kw[:marker_z], kw[:x], kw[:y], kw[:z]) + end + + # map line_z if it's a Function + if isa(get(kw, :line_z, nothing), Function) + kw[:line_z] = isa(kw[:z], Nothing) ? map(kw[:line_z], kw[:x], kw[:y]) : + map(kw[:line_z], kw[:x], kw[:y], kw[:z]) + end + + push!(kw_list, kw) +end + +# Determine axis limits +function RecipesPipeline.get_axis_limits(sc::PlotContext, f, letter) + lims = to_value(data_limits(sc)) + i = if letter === :x + 1 + elseif letter === :y + 2 + elseif letter === :z + 3 + else + throw(ArgumentError("Letter $letter does not correspond to an axis.")) + end + + o = origin(lims) + return (o[i], o[i] + widths(lims)[i]) +end + +######################################## +# Series argument slicing # +######################################## + +function slice_arg(v::AbstractMatrix, idx::Int) + c = mod1(idx, size(v,2)) + m,n = axes(v) + size(v,1) == 1 ? v[first(m),n[c]] : v[:,n[c]] +end +# slice_arg(wrapper::Plots.InputWrapper, idx) = wrapper.obj +slice_arg(v, idx) = v + +# function RecipesPipeline.slice_series_attributes!(sc::PlotContext, kw_list, kw) +# idx = Int(kw[:series_plotindex]) - Int(kw_list[1][:series_plotindex]) + 1 +# +# for k in keys(Plots._series_defaults) +# if haskey(kw, k) +# end +# end +# end + +# Series type conversions + +""" + makie_plottype(st::Symbol) + +Returns the Makie plot type which corresponds to the given seriestype. +The plot type is returned as a Type (`Lines`, `Scatter`, ...). +""" +function makie_plottype(st::Symbol) + return get(makie_seriestype_map, st, Lines) +end + +makie_args(::Type{T}, plotattributes) where T <: AbstractPlot = makie_args(conversion_trait(T), plotattributes) + +function makie_args(::PointBased, plotattributes) + + x, y = (plotattributes[:x], plotattributes[:y]) + + if isempty(x) && isempty(y) + @debug "Encountered an empty series of seriestype $(plotattributes[:seriestype])" + return + end + + if !isnothing(get(plotattributes, :z, nothing)) + return (plotattributes[:x], plotattributes[:y], plotattributes[:z]) + else + return (plotattributes[:x], plotattributes[:y]) + end +end + +# TODO use Makie.plottype +makie_args(::SurfaceLike, plotattributes) = (plotattributes[:x], plotattributes[:y], plotattributes[:z].surf) + +makie_args(::Type{<: Contour}, plotattributes) = (plotattributes[:x], plotattributes[:y], plotattributes[:z].surf) + +function makie_args(::Type{<: Poly}, plotattributes) + return (from_nansep_vec(Point2f0.(plotattributes[:x], plotattributes[:y])),) +end + +function translate_to_makie!(st, pa) + + # general translations first + + # handle colormap + haskey(pa, :cgrad) && (pa[:colormap] = pa[:cgrad]) + + # series color population + haskey(pa, :seriescolor) && (pa[:color] = pa[:seriescolor]) + + haskey(pa, :fill_z) && (pa[:color] = pa[:fill_z]) + pa[:shading] = false # set shading to false, default in Plots + + pa[:linestyle] = get(pa, :linestyle, :auto) + + if pa[:linestyle] ∈ (:auto, :solid) + pa[:linestyle] = nothing + end + + # series color + if st ∈ (:path, :path3d, :curves) + + if !isnothing(get(pa, :line_z, nothing)) + pa[:color] = pa[:line_z] + elseif !isnothing(get(pa, :linecolor, nothing)) + pa[:color] = pa[:linecolor] + elseif !isnothing(get(pa, :seriescolor, nothing)) + pa[:color] = pa[:seriescolor] + end + + pa[:linewidth] = get(pa, :linewidth, 1) + + elseif st == :scatter + if !isnothing(get(pa, :color, nothing)) + # pa[:color] = pa[:color] + end + if !isnothing(get(pa, :marker_z, nothing)) + pa[:color] = pa[:marker_z] + end + if !isnothing(get(pa, :markercolor, nothing)) + pa[:color] = pa[:markercolor] + end + if haskey(pa, :nodecolor) + if pa[:nodecolor] isa Int + pa[:color] = get(pa, :palette, default_palette).colors[pa[:nodecolor]] + else + pa[:color] = pa[:nodecolor] + end + return + end + + if haskey(pa, :markercolor) + if pa[:markercolor] isa Int + pa[:color] = get(pa, :palette, default_palette).colors[pa[:markercolor]] + else + pa[:color] = pa[:markercolor] + end + return + end + if !isnothing(get(pa, :seriescolor, nothing)) + pa[:color] = pa[:seriescolor] + end + + pa[:markersize] = get(pa, :markersize, 5) * 5 * px + + # handle strokes + pa[:strokewidth] = get(pa, :markerstrokewidth, 1) + pa[:strokecolor] = get(pa, :markerstrokecolor, :transparent) + elseif st ∈ (:surface, :heatmap, :image) + haskey(pa, :fill_z) && (pa[:color] = pa[:fill_z]) + pa[:shading] = false # set shading to false, default in Plots + elseif st == :contour + # pa[:levels] = pa[:levels] + elseif st == :bar + haskey(pa, :widths) && (pa[:width] = pa[:widths]) + elseif st == :shape + if haskey(pa, :nodecolor) + if pa[:nodecolor] isa Int + pa[:color] = get(pa, :palette, default_palette).colors[pa[:nodecolor]] + + else + pa[:color] = pa[:nodecolor] + end + return + end + + if haskey(pa, :fillcolor) + if pa[:fillcolor] isa Int + pa[:color] = get(pa, :palette, default_palette).colors[pa[:fillcolor]] + else + pa[:color] = pa[:fillcolor] + end + return + end + + haskey(pa, :fillcolor) && (pa[:color] = pa[:fillcolor]; return) + haskey(pa, :markercolor) && (pa[:color] = pa[:markercolor]; return) + + # handle strokes + pa[:strokewidth] = get(pa, :markerstrokewidth, 1) + pa[:strokecolor] = get(pa, :markerstrokecolor, :transparent) + else + # some default transformations + end + +end + +######################################## +# The real plotting function # +######################################## + +function set_series_color!(scene, st, plotattributes) + + has_color = (haskey(plotattributes, :color) && plotattributes[:color] !== automatic) || any( + if st ∈ (:path, :path3d, :curves) + haskey.(Ref(plotattributes), (:linecolor, :line_z, :seriescolor)) + elseif st == :scatter + haskey.(Ref(plotattributes), (:markercolor, :marker_z, :seriescolor)) + elseif st ∈ (:shape, :heatmap, :image, :surface, :contour, :bar) + haskey.(Ref(plotattributes), (:fillcolor, :fill_z, :seriescolor, :cgrad)) + else # what else? + haskey.(Ref(plotattributes), (:linecolor, :markercolor, :fillcolor, :line_z, :marker_z, :fill_z, :seriescolor)) + end + ) + + has_seriescolor = haskey(plotattributes, :seriescolor) + + if has_color + if haskey(plotattributes, :color) && plotattributes[:color] isa Automatic + delete!(plotattributes, :color) + end + if has_seriescolor + if plotattributes[:seriescolor] ∈ (:match, :auto) + @debug "Assigning new seriescolor from automatic" + delete!(plotattributes, :seriescolor) + # printstyled(st, color=:yellow) + # println() + else + # printstyled(st, color=:green) + # println() + return nothing # series has seriescolor + end + else + # printstyled(st; color = :blue) + # println() + return nothing + end + + + else # TODO FIXME DEBUG REMOVE + # printstyled(st; color = :red) + # println() + end + + if !(plot isa Union{Heatmap, Surface, Image, Spy, Axis2D, Axis3D}) + + get!(plotattributes, :seriescolor, to_color(plotattributes[:palette])) + + end + + return nothing + +end + +function set_palette!(plt, plotattributes) + pt = get!(plotattributes, :palette, default_palette) + if pt isa Palette + # nothing + elseif pt isa Vector{<: Colorant} + plotattributes[:palette] = Palette(pt) + else + @warn "Palette was unrecognizable!" + end +end + +function plot_series_annotations!(plt, args, pt, plotattributes) + + sa = plotattributes[:series_annotations] + + positions = Point2f0.(plotattributes[:x], plotattributes[:y]) + + strs = sa[1] + + bbox_shape = sa[2] + + fontsize = sa[3] + + @debug("Series annotations say hi") + + annotations!(plt, strs, positions; textsize = fontsize/30, align = (:center, :center), color = get(plotattributes, :textcolor, :black)) + +end + +function plot_annotations!(plt, args, pt, plotattributes) + + sa = plotattributes[:annotations] + + positions = Point2f0.(plotattributes[:x], plotattributes[:y]) + + strs = string.(getindex.(sa, 3)) + + fontsizes = Float32.(getindex.(sa, 4)) + + @debug("Annotations say hi") + + annotations!(plt, strs, positions; textsize = fontsizes ./ 80, align = (:center, :center), color = get(plotattributes, :textcolor, :black)) + +end + +function plot_fill!(plt, args, pt, plotattributes) + lowerval, opacity, color = plotattributes[:fill] + upper = plotattributes[:y] + x = plotattributes[:x] + + lower = fill(lowerval, size(x)) + + c = to_color(color) + bandcolor = RGBA(red(c), green(c), blue(c), alpha(c) * opacity) + + band!(plt, x, upper, lower; color = bandcolor) +end + +# Add the "series" to the Scene. +function RecipesPipeline.add_series!(plt::PlotContext, plotattributes) + + # extract the seriestype + st = plotattributes[:seriestype] + + pt = makie_plottype(st) + + theme = default_theme(plt, pt) + + set_palette!(plt, plotattributes) + + set_series_color!(plt, st, plotattributes) + + translate_to_makie!(st, plotattributes) + + args = makie_args(pt, plotattributes) + + for (k, v) in pairs(plotattributes) + isnothing(v) && delete!(plotattributes, k) + end + + ap_attrs = copy(plotattributes) + for (k, v) in pairs(ap_attrs) + haskey(theme, k) || delete!(ap_attrs, k) + end + + # @infiltrate + + if args === nothing + @debug "Found an empty series with type $(plotattributes[:seriestype])." + return plt + end + + plot!(plt, pt, args...; ap_attrs...) + + # handle fill and series annotations after, so they can overdraw + + !isnothing(get(plotattributes, :fill, nothing)) && plot_fill!(plt, args, pt, plotattributes) + + haskey(plotattributes, :annotations) && plot_annotations!(plt, args, pt, plotattributes) + + !isnothing(get(plotattributes, :series_annotations, nothing)) && plot_series_annotations!(plt, args, pt, plotattributes) + + return plt +end diff --git a/MakieRecipes/src/recipeplot.jl b/MakieRecipes/src/recipeplot.jl new file mode 100644 index 00000000000..6c8f6fe1311 --- /dev/null +++ b/MakieRecipes/src/recipeplot.jl @@ -0,0 +1,34 @@ + +@recipe(RecipePlot) do scene + th = merge( + default_theme(scene), + Attributes(palette = Palette(rwong)) + ) + th.color = automatic + return th +end + +function plot!(p::T) where T <: RecipePlot + + # What happens here is that I want to lift on every available observable, + # so they need to be splatted. This also means that nested attributes + # will not be lifted on, but that's an acceptable tradeoff. + # + # After lifting on everything, + + # Node(1) is a dummy observable for dispatch. + lift(Node(1), p.attributes, p.converted, p.converted..., values(p.attributes)...) do _, attrs, args, __lifted... + + !isempty(p.plots) && empty!(p.plots) + + RecipesPipeline.recipe_pipeline!( + p, + Dict{Symbol, Any}(keys(attrs) .=> to_value.(values(attrs))), + to_value.(args) + ) + + end + + return nothing + +end diff --git a/MakieRecipes/test/runtests.jl b/MakieRecipes/test/runtests.jl new file mode 100644 index 00000000000..24dc5957a3f --- /dev/null +++ b/MakieRecipes/test/runtests.jl @@ -0,0 +1,18 @@ +using MakieRecipes +using Literate, MakieCore, CairoMakie +using Test + +cd(@__DIR__) + +@testset "Examples" begin + literatedir = joinpath(@__DIR__, "..", "docs", "src", "literate") + ispath("test_examples") || mkpath("test_examples") + cd("test_examples") do + @test try + include(joinpath(literatedir, "examples.jl")) # execute the source file directly + true + catch + false + end + end +end diff --git a/Project.toml b/Project.toml index 901b7de8c2c..366ae6f49b5 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.13.14" +version = "0.14" [deps] Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" @@ -26,6 +26,7 @@ IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" Isoband = "f1662d9f-8043-43de-a69a-05efc1cc6ff4" KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MakieCore = "20f20a25-4f0e-4fdf-b5d1-57303727442b" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Match = "7eb4fadd-790c-5f42-8a69-bfa0b872bfbf" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" @@ -67,8 +68,9 @@ ImageIO = "0.2, 0.3, 0.4, 0.5" IntervalSets = "0.3, 0.4, 0.5" Isoband = "0.1" KernelDensity = "0.5, 0.6" +MakieCore = "0.1.3" Match = "1.1" -Observables = "0.3.1, 0.4" +Observables = "0.4" Packing = "0.4" PlotUtils = "1" PolygonOps = "0.1.1" diff --git a/test/ReferenceTests/.gitignore b/ReferenceTests/.gitignore similarity index 100% rename from test/ReferenceTests/.gitignore rename to ReferenceTests/.gitignore diff --git a/test/ReferenceTests/Project.toml b/ReferenceTests/Project.toml similarity index 100% rename from test/ReferenceTests/Project.toml rename to ReferenceTests/Project.toml diff --git a/test/ReferenceTests/src/ReferenceTests.jl b/ReferenceTests/src/ReferenceTests.jl similarity index 100% rename from test/ReferenceTests/src/ReferenceTests.jl rename to ReferenceTests/src/ReferenceTests.jl diff --git a/test/ReferenceTests/src/database.jl b/ReferenceTests/src/database.jl similarity index 86% rename from test/ReferenceTests/src/database.jl rename to ReferenceTests/src/database.jl index b496917ff59..4caddc9a722 100644 --- a/test/ReferenceTests/src/database.jl +++ b/ReferenceTests/src/database.jl @@ -1,86 +1,95 @@ - -struct Entry - title::String - source_location::LineNumberNode - code::Expr - func::Function - used_functions::Set{Symbol} -end - -function Entry(title, source_location, code, func) - used_functions = Set{Symbol}() - MacroTools.postwalk(code) do x - if @capture(x, f_(xs__)) - push!(used_functions, Symbol(string(f))) - end - if @capture(x, f_(xs__) do; body__; end) - push!(used_functions, Symbol(string(f))) - end - return x - end - return Entry(title, source_location, code, func, used_functions) -end - -function unique_name(entry::Entry) - loc = entry.source_location - filename = splitext(basename(string(loc.file)))[1] - sep = isempty(entry.title) ? "" : "_" - return replace(string(filename, "_", loc.line, sep, entry.title), " " => "") -end - -function nice_title(entry::Entry) - isempty(entry.title) || return entry.title - return unique_name(entry) -end - -const DATABASE = Dict{String, Entry}() - -function cell_expr(name, code, source) - key = string(source.file, ":", source.line) - return quote - closure = () -> $(esc(code)) - entry = ReferenceTests.Entry( - $(string(name)), - $(QuoteNode(source)), - $(QuoteNode(code)), - closure - ) - ReferenceTests.DATABASE[$key] = entry - # display(closure()) - end -end - -macro cell(name, code) - return cell_expr(name, code, __source__) -end - -macro cell(code) - return cell_expr("", code, __source__) -end - -""" - save_result(path, object) - -Helper, to more easily save all kind of results from the test database -""" -function save_result(path::String, scene::Makie.FigureLike) - FileIO.save(path * ".png", scene) -end - -function save_result(path::String, stream::VideoStream) - FileIO.save(path * ".mp4", stream) -end - -function save_result(path::String, object) - FileIO.save(path, object) -end - -function load_database() - empty!(DATABASE) - include(joinpath(@__DIR__, "tests/text.jl")) - include(joinpath(@__DIR__, "tests/attributes.jl")) - include(joinpath(@__DIR__, "tests/examples2d.jl")) - include(joinpath(@__DIR__, "tests/examples3d.jl")) - include(joinpath(@__DIR__, "tests/short_tests.jl")) - return DATABASE -end + +struct Entry + title::String + source_location::LineNumberNode + code::Expr + func::Function + used_functions::Set{Symbol} +end + +function Entry(title, source_location, code, func) + used_functions = Set{Symbol}() + MacroTools.postwalk(code) do x + if @capture(x, f_(xs__)) + push!(used_functions, Symbol(string(f))) + end + if @capture(x, f_(xs__) do; body__; end) + push!(used_functions, Symbol(string(f))) + end + return x + end + return Entry(title, source_location, code, func, used_functions) +end + +function unique_name(entry::Entry) + loc = entry.source_location + filename = splitext(basename(string(loc.file)))[1] + sep = isempty(entry.title) ? "" : "_" + return replace(string(filename, "_", loc.line, sep, entry.title), " " => "") +end + +function nice_title(entry::Entry) + isempty(entry.title) || return entry.title + return unique_name(entry) +end + +const DATABASE = Dict{String, Entry}() + +function cell_expr(name, code, source) + key = string(source.file, ":", source.line) + return quote + closure = () -> $(esc(code)) + entry = ReferenceTests.Entry( + $(string(name)), + $(QuoteNode(source)), + $(QuoteNode(code)), + closure + ) + ReferenceTests.DATABASE[$key] = entry + # display(closure()) + end +end + +macro cell(name, code) + return cell_expr(name, code, __source__) +end + +macro cell(code) + return cell_expr("", code, __source__) +end + +""" + save_result(path, object) + +Helper, to more easily save all kind of results from the test database +""" +function save_result(path::String, scene::Makie.FigureLike) + FileIO.save(path * ".png", scene) +end + +function save_result(path::String, stream::VideoStream) + FileIO.save(path * ".mp4", stream) +end + +function save_result(path::String, object) + FileIO.save(path, object) +end + +function load_database() + empty!(DATABASE) + include(joinpath(@__DIR__, "tests/text.jl")) + include(joinpath(@__DIR__, "tests/attributes.jl")) + include(joinpath(@__DIR__, "tests/examples2d.jl")) + include(joinpath(@__DIR__, "tests/examples3d.jl")) + include(joinpath(@__DIR__, "tests/short_tests.jl")) + return DATABASE +end + +function database_filtered(title_excludes = [], nice_title_excludes = []; functions=[]) + database = ReferenceTests.load_database() + return filter(database) do (name, entry) + !(entry.title in title_excludes) && + !(nice_title(entry) in nice_title_excludes) && + !any(x-> x in entry.used_functions, functions) + end +end diff --git a/test/ReferenceTests/src/html_rendering.jl b/ReferenceTests/src/html_rendering.jl similarity index 100% rename from test/ReferenceTests/src/html_rendering.jl rename to ReferenceTests/src/html_rendering.jl diff --git a/test/ReferenceTests/src/image_download.jl b/ReferenceTests/src/image_download.jl similarity index 95% rename from test/ReferenceTests/src/image_download.jl rename to ReferenceTests/src/image_download.jl index 440e00ea045..5c33ffbee91 100644 --- a/test/ReferenceTests/src/image_download.jl +++ b/ReferenceTests/src/image_download.jl @@ -7,7 +7,7 @@ end # Well, to be more precise, last non patch function last_major_version() - path = basedir("..", "..", "Project.toml") + path = basedir("..", "Project.toml") version = VersionNumber(TOML.parse(String(read(path)))["version"]) return "v" * string(VersionNumber(version.major, version.minor)) end diff --git a/test/ReferenceTests/src/runtests.jl b/ReferenceTests/src/runtests.jl similarity index 100% rename from test/ReferenceTests/src/runtests.jl rename to ReferenceTests/src/runtests.jl diff --git a/test/ReferenceTests/src/stable_rng.jl b/ReferenceTests/src/stable_rng.jl similarity index 100% rename from test/ReferenceTests/src/stable_rng.jl rename to ReferenceTests/src/stable_rng.jl diff --git a/test/ReferenceTests/src/tests/attributes.jl b/ReferenceTests/src/tests/attributes.jl similarity index 100% rename from test/ReferenceTests/src/tests/attributes.jl rename to ReferenceTests/src/tests/attributes.jl diff --git a/test/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl similarity index 100% rename from test/ReferenceTests/src/tests/examples2d.jl rename to ReferenceTests/src/tests/examples2d.jl diff --git a/test/ReferenceTests/src/tests/examples3d.jl b/ReferenceTests/src/tests/examples3d.jl similarity index 100% rename from test/ReferenceTests/src/tests/examples3d.jl rename to ReferenceTests/src/tests/examples3d.jl diff --git a/test/ReferenceTests/src/tests/figures_and_makielayout.jl b/ReferenceTests/src/tests/figures_and_makielayout.jl similarity index 100% rename from test/ReferenceTests/src/tests/figures_and_makielayout.jl rename to ReferenceTests/src/tests/figures_and_makielayout.jl diff --git a/test/ReferenceTests/src/tests/recipes.jl b/ReferenceTests/src/tests/recipes.jl similarity index 100% rename from test/ReferenceTests/src/tests/recipes.jl rename to ReferenceTests/src/tests/recipes.jl diff --git a/test/ReferenceTests/src/tests/short_tests.jl b/ReferenceTests/src/tests/short_tests.jl similarity index 100% rename from test/ReferenceTests/src/tests/short_tests.jl rename to ReferenceTests/src/tests/short_tests.jl diff --git a/test/ReferenceTests/src/tests/text.jl b/ReferenceTests/src/tests/text.jl similarity index 100% rename from test/ReferenceTests/src/tests/text.jl rename to ReferenceTests/src/tests/text.jl diff --git a/test/ReferenceTests/src/visual-regression.jl b/ReferenceTests/src/visual-regression.jl similarity index 100% rename from test/ReferenceTests/src/visual-regression.jl rename to ReferenceTests/src/visual-regression.jl diff --git a/WGLMakie/LICENSE b/WGLMakie/LICENSE new file mode 100644 index 00000000000..31f214587ed --- /dev/null +++ b/WGLMakie/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Simon Danisch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/WGLMakie/Project.toml b/WGLMakie/Project.toml new file mode 100644 index 00000000000..bf6781b1908 --- /dev/null +++ b/WGLMakie/Project.toml @@ -0,0 +1,40 @@ +authors = ["SimonDanisch "] +name = "WGLMakie" +uuid = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" +version = "0.4" + +[deps] +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +FreeTypeAbstraction = "663a7486-cb36-511b-a19d-713bb74d65c9" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +Hyperscript = "47d2ed2b-36de-50cf-bf87-49c2cf4b8b91" +ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +JSServe = "824d6782-a2ef-11e9-3a09-e5662e0c26f9" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Observables = "510215fc-4207-5dde-b226-833fc4488ee2" +ShaderAbstractions = "65257c39-d410-5151-9873-9b3e5be5013e" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[compat] +Colors = "0.11, 0.12" +FileIO = "1.1" +FreeTypeAbstraction = "0.8, 0.9" +GeometryBasics = "0.3" +Hyperscript = "0.0.3, 0.0.4" +ImageMagick = "1.1" +JSServe = "1.2" +Makie = "=0.14.0" +Observables = "0.4" +ShaderAbstractions = "0.2.1" +StaticArrays = "0.12, 1.0" +julia = "1.3" + +[extras] +ElectronDisplay = "d872a56f-244b-5cc9-b574-2017b5b909a8" +MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test", "ElectronDisplay", "MeshIO"] diff --git a/WGLMakie/README.md b/WGLMakie/README.md new file mode 100644 index 00000000000..404b9b20d81 --- /dev/null +++ b/WGLMakie/README.md @@ -0,0 +1,37 @@ +WGLMakie is a WebGL backend for the [Makie.jl](https://www.github.com/JuliaPlots/Makie.jl) plotting package, implemented using Three.js. + +Read the docs for Makie and it's backends [here](http://makie.juliaplots.org/.dev) + +# Usage + +Now, it should just work like Makie: + +```julia +using WGLMakie +scatter(rand(4)) +``` + +In the REPL, this will open a browser tab, that will refresh on a new display. +In VSCode, this should open in the plotpane. +You can also embed plots in a JSServe webpage: + +```julia +function dom_handler(session, request) + return DOM.div( + DOM.h1("Some Makie Plots:"), + meshscatter(1:4, color=1:4), + meshscatter(1:4, color=rand(RGBAf0, 4)), + meshscatter(1:4, color=rand(RGBf0, 4)), + meshscatter(1:4, color=:red), + meshscatter(rand(Point3f0, 10), color=rand(RGBf0, 10)), + meshscatter(rand(Point3f0, 10), marker=Pyramid(Point3f0(0), 1f0, 1f0)), + ) +end +isdefined(Main, :app) && close(app) +app = JSServe.Server(dom_handler, "127.0.0.1", 8082) +``` + +## Sponsors + + +Förderkennzeichen: 01IS10S27, 2020 diff --git a/WGLMakie/assets/line_segments.frag b/WGLMakie/assets/line_segments.frag new file mode 100644 index 00000000000..15e1e516bc8 --- /dev/null +++ b/WGLMakie/assets/line_segments.frag @@ -0,0 +1,6 @@ + +in vec4 frag_color; + +void main() { + fragment_color = frag_color; +} diff --git a/WGLMakie/assets/line_segments.vert b/WGLMakie/assets/line_segments.vert new file mode 100644 index 00000000000..e1cf02639a6 --- /dev/null +++ b/WGLMakie/assets/line_segments.vert @@ -0,0 +1,47 @@ +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; + +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); + // I think GLMakie is drawing the lines too thick... + // untill we figure out who is right, we need to add 1.0 to linewidth + thickness = thickness > 0.0 ? thickness + 1.0 : 0.0; + normal *= (((thickness) / 2.0) / 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; + +} diff --git a/WGLMakie/assets/lines.frag b/WGLMakie/assets/lines.frag new file mode 100644 index 00000000000..e69de29bb2d diff --git a/WGLMakie/assets/mesh.frag b/WGLMakie/assets/mesh.frag new file mode 100644 index 00000000000..abbfe58a861 --- /dev/null +++ b/WGLMakie/assets/mesh.frag @@ -0,0 +1,71 @@ +in vec2 frag_uv; +in vec4 frag_color; + +in vec3 o_normal; +in vec3 o_camdir; +in vec3 o_lightdir; + +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), get_shininess()); + + // final lighting model + return vec3( + get_ambient() * color + + get_diffuse() * diff_coeff * color + + get_specular() * spec_coeff + ); +} + +vec4 get_color(vec3 color, vec2 uv, bool colorrange, bool colormap){ + return vec4(color, 1.0); // we must prohibit uv from getting into dead variable removal +} + +vec4 get_color(vec4 color, vec2 uv, bool colorrange, bool colormap){ + return color; // we must prohibit uv from getting into dead variable removal +} + +vec4 get_color(bool color, vec2 uv, bool colorrange, bool colormap){ + return frag_color; // color not in uniform +} + +vec4 get_color(sampler2D color, vec2 uv, bool colorrange, bool colormap){ + return texture(color, uv); +} + +float _normalize(float val, float from, float to){return (val-from) / (to - from);} + +vec4 get_color(sampler2D color, vec2 uv, vec2 colorrange, sampler2D colormap){ + float value = texture(color, uv).x; + float normed = _normalize(value, colorrange.x, colorrange.y); + vec4 c = texture(colormap, vec2(normed, 0.0)); + + if (isnan(value)) { + c = get_nan_color(); + } else if (value < colorrange.x) { + c = get_lowclip(); + } else if (value > colorrange.y) { + c = get_highclip(); + } + return c; +} + +vec4 get_color(sampler2D color, vec2 uv, bool colorrange, sampler2D colormap){ + return texture(color, uv); +} + +void main() { + vec4 real_color = get_color(uniform_color, frag_uv, get_colorrange(), colormap); + vec3 shaded_color = real_color.xyz; + + if(get_shading()){ + vec3 L = normalize(o_lightdir); + vec3 N = normalize(o_normal); + shaded_color = blinnphong(N, o_camdir, L, real_color.rgb); + } + fragment_color = vec4(shaded_color, real_color.a); +} diff --git a/WGLMakie/assets/mesh.vert b/WGLMakie/assets/mesh.vert new file mode 100644 index 00000000000..44f28b8a117 --- /dev/null +++ b/WGLMakie/assets/mesh.vert @@ -0,0 +1,45 @@ +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; + +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;} + + + +void main(){ + // get_* gets the global inputs (uniform, sampler, position array) + // those functions will get inserted by the shader creation pipeline + vec3 vertex_position = tovec3(get_position()); + if (isnan(vertex_position.z)) { + vertex_position.z = 0.0; + } + vec4 position_world = model * vec4(vertex_position, 1); + + // normal in world space + o_normal = get_normalmatrix() * get_normals(); + // 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; + // direction to light + o_lightdir = normalize(view*vec4(get_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; + + frag_uv = get_uv(); + frag_uv = vec2(1.0 - frag_uv.y, frag_uv.x); + frag_color = tovec4(get_color()); +} diff --git a/WGLMakie/assets/particles.frag b/WGLMakie/assets/particles.frag new file mode 100644 index 00000000000..eb5ae5ba304 --- /dev/null +++ b/WGLMakie/assets/particles.frag @@ -0,0 +1,27 @@ +in vec4 frag_color; +in vec3 frag_normal; +in vec3 frag_position; +in vec3 frag_lightdir; + +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), 8.0); + 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 + ); +} + +void main() { + vec3 color = blinnphong(frag_normal, frag_position, frag_lightdir, frag_color.xyz); + fragment_color = vec4(color, frag_color.a); +} diff --git a/WGLMakie/assets/particles.vert b/WGLMakie/assets/particles.vert new file mode 100644 index 00000000000..76f20eba081 --- /dev/null +++ b/WGLMakie/assets/particles.vert @@ -0,0 +1,45 @@ +precision mediump float; + +uniform mat4 projection; +uniform mat4 view; + + +out vec3 frag_normal; +out vec3 frag_position; + +out vec4 frag_color; +out vec3 frag_lightdir; + + +vec3 qmul(vec4 q, vec3 v){ + return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v); +} + +void rotate(vec4 q, inout vec3 V, inout vec3 N){ + V = qmul(q, V); + N = normalize(qmul(q, N)); +} + +vec4 to_vec4(vec3 v3){return vec4(v3, 1.0);} +vec4 to_vec4(vec4 v4){return v4;} + +vec3 to_vec3(vec2 v3){return vec3(v3, 0.0);} +vec3 to_vec3(vec3 v4){return v4;} + +void main(){ + // get_* gets the global inputs (uniform, sampler, position array) + // those functions will get inserted by the shader creation pipeline + vec3 vertex_position = get_markersize() * get_position(); + vec3 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; + // screen space coordinates of the position + gl_Position = projection * view * position_world; +} diff --git a/WGLMakie/assets/simple.frag b/WGLMakie/assets/simple.frag new file mode 100644 index 00000000000..40114ef9ea2 --- /dev/null +++ b/WGLMakie/assets/simple.frag @@ -0,0 +1,9 @@ +precision mediump float; + +in vec3 frag_normal; +in vec3 frag_position; +in vec3 frag_lightdir; + +void main() { + fragment_color = vec4(1, 0, 1, 1); +} diff --git a/WGLMakie/assets/simple.vert b/WGLMakie/assets/simple.vert new file mode 100644 index 00000000000..39d0b158870 --- /dev/null +++ b/WGLMakie/assets/simple.vert @@ -0,0 +1,114 @@ +uniform mat4 projection; +uniform mat4 view; + +uniform float atlas_tex_dim; + +out vec4 frag_color; +out vec2 frag_uv; +out float frag_uvscale; +out float frag_distancefield_scale; +out vec4 frag_uv_offset_width; + + +mat4 qmat(vec4 quat){ + float num = quat.x * 2.0; + float num2 = quat.y * 2.0; + float num3 = quat.z * 2.0; + float num4 = quat.x * num; + float num5 = quat.y * num2; + float num6 = quat.z * num3; + float num7 = quat.x * num2; + float num8 = quat.x * num3; + float num9 = quat.y * num3; + float num10 = quat.w * num; + float num11 = quat.w * num2; + float num12 = quat.w * num3; + return mat4( + (1.0 - (num5 + num6)), (num7 + num12), (num8 - num11), 0.0, + (num7 - num12), (1.0 - (num4 + num6)), (num9 + num10), 0.0, + (num8 + num11), (num9 - num10), (1.0 - (num4 + num5)), 0.0, + 0.0, 0.0, 0.0, 1.0 + ); +} + +float distancefield_scale(){ + // Glyph distance field units are in pixels; convert to dimensionless + // x-coordinate of texture instead for consistency with programmatic uv + // distance fields in fragment shader. See also comments below. + vec4 uv_rect = get_uv_offset_width(); + float pixsize_x = (uv_rect.z - uv_rect.x) * get_atlas_texture_size(); + return -1.0/pixsize_x; +} + +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;} + +mat2 diagm(vec2 v){ + return mat2(v.x, 0.0, 0.0, v.y); +} +float _determinant(mat2 m) { + return m[0][0] * m[1][1] - m[0][1] * m[1][0]; +} +void main(){ + vec2 bbox_signed_radius = 0.5 * get_markersize(); // note; components may be negative. + vec2 sprite_bbox_centre = get_marker_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); + mat4 billtrans = get_use_pixel_marker() ? pixelspace : projection; + trans = (get_billboard() ? billtrans : pview) * qmat(get_rotations()) * trans; + + // Compute centre of billboard in clipping coordinates + vec4 sprite_center = trans * vec4(sprite_bbox_centre, 0, 0); + vec4 data_point = pview * model * vec4(tovec3(get_offset()), 1); + vec4 vclip = data_point + sprite_center; + + // Extra buffering is required around sprites which are antialiased so that + // the antialias blur doesn't get cut off (see #15). This blur falls to + // zero at a radius of ANTIALIAS_RADIUS pixels in the viewport coordinates + // and we want to buffer the vertices in the *source* sprite coordinate + // system so that we get this amount in the output coordinates. + // + // Here we calculate the derivative of the mapping from sprite xy + // coordinates (defined by `trans`) into the viewport pixel coordinates. + // The derivative needs to include the proper term for the perspective + // divide into NDC, evaluated at the centre point `vclip`. + mat4 d_ndc_d_clip = mat4( + 1.0/vclip.w, 0.0, 0.0, 0.0, + 0.0, 1.0/vclip.w, 0.0, 0.0, + 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); + // 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 + // transformations, the geometric mean of the two principle scale factors + // is a reasonable compromise: + float viewport_from_sprite_scale = sqrt(abs(_determinant(dxyv_dxys))); + + // In the fragment shader we want our signed distance in viewport (pixel) + // coords for direct use in antialiasing step functions. We therefore need + // a scaling factor similar to viewport_from_sprite_scale, but including + // the uv->sprite coordinate system scaling factor as well. We choose to + // use the bounding box *x* width for this. This comes with some + // consistency conditions: + // * For procedural distance fields, we need the sprite bounding box to be + // square. (If not, the uv coordinates will be anisotropically scaled and + // any calculation based on them will not be a distance function.) + // * For sampled distance fields, we need to consistently choose the *x* + // for the scaling in get_distancefield_scale(). + float sprite_from_u_scale = abs(get_markersize().x); + frag_uvscale = viewport_from_sprite_scale * sprite_from_u_scale; + frag_distancefield_scale = distancefield_scale(); + frag_color = tovec4(get_color()); + frag_uv = get_uv(); + frag_uv_offset_width = get_uv_offset_width(); + // screen space coordinates of the position + vec4 quad_vertex = (trans * vec4(2.0 * bbox_signed_radius * get_position(), 0.0, 0.0)); + gl_Position = vclip + quad_vertex; +} diff --git a/WGLMakie/assets/sprites.frag b/WGLMakie/assets/sprites.frag new file mode 100644 index 00000000000..ecf78e368c7 --- /dev/null +++ b/WGLMakie/assets/sprites.frag @@ -0,0 +1,89 @@ +in vec4 frag_color; +in vec2 frag_uv; + +#define CIRCLE 0 +#define RECTANGLE 1 +#define ROUNDED_RECTANGLE 2 +#define DISTANCEFIELD 3 +#define TRIANGLE 4 + +#define M_SQRT_2 1.4142135 + + +// Half width of antialiasing smoothstep +#define ANTIALIAS_RADIUS 0.8 +// These versions of aastep assume that `dist` is a signed distance function +// which has been scaled to be in units of pixels. +float aastep(float threshold1, float dist) { + return smoothstep(threshold1-ANTIALIAS_RADIUS, threshold1 + ANTIALIAS_RADIUS, dist); +} + +float aastep(float threshold1, float threshold2, float dist) { + return smoothstep(threshold1-ANTIALIAS_RADIUS, threshold1+ANTIALIAS_RADIUS, dist) - + smoothstep(threshold2-ANTIALIAS_RADIUS, threshold2+ANTIALIAS_RADIUS, dist); +} + +// Procedural signed distance functions on the uv coordinate patch [0,1]x[0,1] +// Note that for antialiasing to work properly these should be *scale preserving* +// (If you must rescale uv, make sure to put the scale factor back in later.) +float triangle(vec2 P){ + P -= vec2(0.5); + float x = M_SQRT_2 * (P.x - P.y); + float y = M_SQRT_2 * (P.x + P.y); + float r1 = max(abs(x), abs(y)) - 1.0/(2.0*M_SQRT_2); + float r2 = P.y; + return -max(r1,r2); +} +float circle(vec2 uv){ + return 0.5-length(uv-vec2(0.5)); +} +float rectangle(vec2 uv){ + vec2 d = max(-uv, uv-vec2(1)); + return -((length(max(vec2(0.0), d)) + min(0.0, max(d.x, d.y)))); +} +float rounded_rectangle(vec2 uv, vec2 tl, vec2 br){ + vec2 d = max(tl-uv, uv-br); + return -((length(max(vec2(0.0), d)) + min(0.0, max(d.x, d.y)))-tl.x); +} + +void fill(vec4 fillcolor, vec2 uv, float infill, inout vec4 color){ + color = mix(color, fillcolor, infill); +} + +in float frag_uvscale; +in float frag_distancefield_scale; +in vec4 frag_uv_offset_width; + +float scaled_distancefield(sampler2D distancefield, vec2 uv){ + // Glyph distance field units are in pixels. Convert to same distance + // scaling as f_uv.x for consistency with the procedural signed_distance + // calculations. + return frag_distancefield_scale * texture(distancefield, uv).r; +} + +float scaled_distancefield(bool distancefield, vec2 uv){ + return 0.0; +} + +void main() { + int shape = get_shape_type(); + float signed_distance = 0.0; + vec4 uv_off = frag_uv_offset_width; + vec2 tex_uv = mix(uv_off.xy, uv_off.zw, clamp(frag_uv, 0.0, 1.0)); + if(shape == CIRCLE) + signed_distance = circle(frag_uv); + else if(shape == DISTANCEFIELD) + signed_distance = scaled_distancefield(distancefield, tex_uv); + else if(shape == ROUNDED_RECTANGLE) + signed_distance = rounded_rectangle(frag_uv, vec2(0.2), vec2(0.8)); + else if(shape == RECTANGLE) + signed_distance = 1.0; // rectangle(f_uv); + else if(shape == TRIANGLE) + signed_distance = triangle(frag_uv); + + signed_distance *= frag_uvscale; + float inside = aastep(0.0, signed_distance); + vec4 final_color = vec4(frag_color.xyz, 0); + fill(frag_color, frag_uv, inside, final_color); + fragment_color = final_color; +} diff --git a/WGLMakie/assets/surface.vert b/WGLMakie/assets/surface.vert new file mode 100644 index 00000000000..c0eadf5168f --- /dev/null +++ b/WGLMakie/assets/surface.vert @@ -0,0 +1,47 @@ +struct Grid2D{ + ivec2 lendiv; + vec2 start; + vec2 stop; + ivec2 dims; +}; + +attribute vec2 position; +uniform sampler2D position_z; + +uniform mat4 view, model, projection; + +uniform bool wireframe; +uniform uint objectid; +uniform float stroke_width; +flat out uvec2 o_id; +out vec4 o_color; +out vec2 o_uv; + +flat out vec2 f_scale; +flat out vec4 f_color; +flat out vec4 f_bg_color; +flat out vec4 f_stroke_color; +flat out vec4 f_glow_color; +flat out int f_primitive_index; +flat out uvec2 f_id; + +out vec2 f_uv; +out vec2 f_uv_offset; + +void main() +{ + int index = gl_InstanceID; + vec2 offset = vertices; + ivec2 offseti = ivec2(offset); + ivec2 dims = textureSize(position_z, 0); + vec2 final_scale = ((scale.xy)/(scale.xy-stroke_width)); + + const float uv_w = 0.9; + + vec3 pos; + {{position_calc}} + o_color = get_color(color, pos.z, color_map, color_norm, index); + o_uv = index01; + vec3 normalvec = {{normal_calc}}; + render(model * vec4(pos, 1), (model * vec4(normalvec, 0)).xyz, view, projection, light); +} diff --git a/WGLMakie/assets/volume.frag b/WGLMakie/assets/volume.frag new file mode 100644 index 00000000000..5fcc8b703d7 --- /dev/null +++ b/WGLMakie/assets/volume.frag @@ -0,0 +1,263 @@ +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; + +const float max_distance = 1.3; + +const int num_samples = 200; +const float step_size = max_distance / float(num_samples); + +float _normalize(float val, float from, float to) +{ + return (val-from) / (to - from); +} + +vec4 color_lookup(float intensity, sampler2D color_ramp, vec2 norm) +{ + return texture(color_ramp, vec2(_normalize(intensity, norm.x, norm.y), 0.0)); +} + +vec3 gennormal(vec3 uvw, float d) +{ + vec3 a, b; + // handle normals at edges! + if(uvw.x + d >= 1.0){ + return vec3(1, 0, 0); + } + if(uvw.y + d >= 1.0){ + return vec3(0, 1, 0); + } + if(uvw.z + d >= 1.0){ + return vec3(0, 0, 1); + } + + if(uvw.x - d <= 0.0){ + return vec3(-1, 0, 0); + } + if(uvw.y - d <= 0.0){ + return vec3(0, -1, 0); + } + if(uvw.z - d <= 0.0){ + 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.y = texture(volumedata, uvw - vec3(0.0,d,0.0)).r; + b.y = texture(volumedata, uvw + vec3(0.0,d,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); +} + +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); + // final lighting model + return vec3( + ambient * color + + diffuse * diff_coeff * color + + specular * spec_coeff + ); +} + +// Simple random generator found: http://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl +float rand(){ + return fract(sin(gl_FragCoord.x * 12.9898 + gl_FragCoord.y * 78.233) * 43758.5453); +} + +vec4 volume(vec3 front, vec3 dir) +{ + // The per-voxel alpha channel is specified in units of opacity/length. + // If our voxels are not isotropic, then the distance that we trace through + // depends on the direction. + vec3 pos = front; + float T = 1.0; + vec3 Lo = vec3(0.0); + int i = 0; + for (i; i < num_samples; ++i) { + float intensity = texture(volumedata, pos).x; + vec4 density = color_lookup(intensity, colormap, colorrange); + float opacity = step_size * density.a * absorption; + T *= 1.0 - opacity; + if (T <= 0.01) + break; + + Lo += (T*opacity)*density.rgb; + pos += dir; + } + return vec4(Lo, 1.0 - T); +} + + +vec4 volumergba(vec3 front, vec3 dir) +{ + vec3 pos = front; + float T = 1.0; + vec3 Lo = vec3(0.0); + int i = 0; + for (i; i < num_samples ; ++i) { + vec4 density = texture(volumedata, pos); + float opacity = step_size * density.a; + T *= 1.0 - opacity; + if (T <= 0.01) + break; + + Lo += (T*opacity)*density.rgb; + pos += dir; + } + return vec4(Lo, 1.0 - T); +} + +// vec4 volumeindexedrgba(vec3 front, vec3 dir) +// { +// vec3 pos = front; +// float T = 1.0; +// vec3 Lo = vec3(0.0); +// int i = 0; +// for (i; i < num_samples; ++i) { +// int index = int(texture(volumedata, pos).x) - 1; +// vec4 density = color_lookup(colormap, index); +// float opacity = step_size*density.a; +// Lo += (T*opacity)*density.rgb; +// T *= 1.0 - opacity; +// if (T <= 0.01) +// break; +// pos += dir; +// } +// return vec4(Lo, 1-T); +// } + +vec4 contours(vec3 front, vec3 dir) +{ + vec3 pos = front; + float T = 1.0; + vec3 Lo = vec3(0.0); + int i = 0; + 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 opaque = blinnphong(N, camdir, L, density.rgb); + Lo += (T * opacity) * opaque; + T *= 1.0 - opacity; + if (T <= 0.01) + break; + } + pos += dir; + } + return vec4(Lo, 1.0 - T); +} + +vec4 isosurface(vec3 front, vec3 dir) +{ + vec3 pos = front; + vec4 c = vec4(0.0); + int i = 0; + vec4 diffuse_color = color_lookup(isovalue, colormap, colorrange); + 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); + // back & frontface... + vec3 c1 = blinnphong(N, camdir, L, diffuse_color.rgb); + vec3 c2 = blinnphong(-N, camdir, L, diffuse_color.rgb); + c = vec4(0.5*c1 + 0.5*c2, diffuse_color.a); + break; + } + pos += dir; + } + return c; +} + +vec4 mip(vec3 front, vec3 dir) +{ + vec3 pos = front; + int i = 0; + float maximum = 0.0; + for (i; i < num_samples; ++i, pos += dir){ + float density = texture(volumedata, pos).x; + if(maximum < density) + maximum = density; + } + return color_lookup(maximum, colormap, colorrange); +} + +uniform uint objectid; + +void write2framebuffer(vec4 color, uvec2 id); + +const float typemax = 100000000000000000000000000000000000000.0; + +bool no_solution(float x){ + return x <= 0.0001 || isinf(x) || isnan(x); +} + +float min_bigger_0(float a, float b){ + bool a_no = no_solution(a); + bool b_no = no_solution(b); + if(a_no && b_no){ + // no solution + return typemax; + } + if(a_no){ + return b; + } + if(b_no){ + return a; + } + return min(a, b); +} + +float min_bigger_0(vec3 v1, vec3 v2){ + float x = min_bigger_0(v1.x, v2.x); + float y = min_bigger_0(v1.y, v2.y); + float z = min_bigger_0(v1.z, v2.z); + return min(x, min(y, z)); +} + +void main() +{ + vec4 color; + vec3 eye_unit = vec3(modelinv * vec4(eyeposition, 1)); + vec3 back_position = frag_vert; + vec3 dir = normalize(eye_unit - back_position); + // solve back_position + distance * dir == 1 + // solve back_position + distance * dir == 0 + // to see where it first hits unit cube! + vec3 solution_1 = (1.0 - back_position) / dir; + vec3 solution_0 = (0.0 - back_position) / dir; + float solution = min_bigger_0(solution_1, solution_0); + + vec3 start = back_position + solution * dir; + vec3 step_in_dir = (back_position - start) / float(num_samples); + + float steps = 0.1; + if(algorithm == uint(0)) + color = isosurface(start, step_in_dir); + else if(algorithm == uint(1)) + color = volume(start, step_in_dir); + else if(algorithm == uint(2)) + color = mip(start, step_in_dir); + else if(algorithm == uint(3)) + color = volumergba(start, step_in_dir); + else if(algorithm == uint(4)) + color = vec4(0.0); + // color = volumeindexedrgba(start, step_in_dir); + else + color = contours(start, step_in_dir); + + fragment_color = color; +} diff --git a/WGLMakie/assets/volume.vert b/WGLMakie/assets/volume.vert new file mode 100644 index 00000000000..3228940ae3c --- /dev/null +++ b/WGLMakie/assets/volume.vert @@ -0,0 +1,12 @@ +out vec3 frag_vert; +out vec3 o_light_dir; + +uniform mat4 projection, view; + +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; +} diff --git a/WGLMakie/src/WEBGL.js b/WGLMakie/src/WEBGL.js new file mode 100644 index 00000000000..1ddb9cad6f6 --- /dev/null +++ b/WGLMakie/src/WEBGL.js @@ -0,0 +1,73 @@ +// Taken from THREEJS documentation +const WEBGL = { + isWebGLAvailable: function () { + try { + var canvas = document.createElement("canvas"); + return !!( + window.WebGLRenderingContext && + (canvas.getContext("webgl") || + canvas.getContext("experimental-webgl")) + ); + } catch (e) { + return false; + } + }, + + isWebGL2Available: function () { + try { + var canvas = document.createElement("canvas"); + return !!( + window.WebGL2RenderingContext && canvas.getContext("webgl2") + ); + } catch (e) { + return false; + } + }, + + getWebGLErrorMessage: function () { + return this.getErrorMessage(1); + }, + + getWebGL2ErrorMessage: function () { + return this.getErrorMessage(2); + }, + + getErrorMessage: function (version) { + var names = { + 1: "WebGL", + 2: "WebGL 2", + }; + + var contexts = { + 1: window.WebGLRenderingContext, + 2: window.WebGL2RenderingContext, + }; + + var message = + 'Your $0 does not seem to support $1'; + + var element = document.createElement("div"); + element.id = "webglmessage"; + element.style.fontFamily = "monospace"; + element.style.fontSize = "13px"; + element.style.fontWeight = "normal"; + element.style.textAlign = "center"; + element.style.background = "#fff"; + element.style.color = "#000"; + element.style.padding = "1.5em"; + element.style.width = "400px"; + element.style.margin = "5em auto 0"; + + if (contexts[version]) { + message = message.replace("$0", "graphics card"); + } else { + message = message.replace("$0", "browser"); + } + + message = message.replace("$1", names[version]); + + element.innerHTML = message; + + return element; + }, +} diff --git a/WGLMakie/src/WGLMakie.jl b/WGLMakie/src/WGLMakie.jl new file mode 100644 index 00000000000..db7339ddc46 --- /dev/null +++ b/WGLMakie/src/WGLMakie.jl @@ -0,0 +1,77 @@ +module WGLMakie + +using Hyperscript +using JSServe +using Observables +using Makie +using Colors +using ShaderAbstractions +using LinearAlgebra +using GeometryBasics +using ImageMagick +using FreeTypeAbstraction +using StaticArrays + +using JSServe: Session +using JSServe: @js_str, onjs, Dependency, App +using JSServe.DOM + +using ShaderAbstractions: VertexArray, Buffer, Sampler, AbstractSampler +using ShaderAbstractions: InstancedProgram + +import Makie.FileIO +using Makie: get_texture_atlas, glyph_uv_width!, SceneSpace, Pixel +using Makie: attribute_per_char, glyph_uv_width!, layout_text +using Makie: MouseButtonEvent, KeyEvent +using Makie: apply_transform, transform_func_obs +using Makie: inline! + +struct WebGL <: ShaderAbstractions.AbstractContext end +struct WGLBackend <: Makie.AbstractBackend end +#["https://unpkg.com/three@0.123.0/build/three.min.js" +const THREE = Dependency(:THREE, + ["https://cdn.jsdelivr.net/gh/mrdoob/three.js/build/three.js"]) + +const WGL = Dependency(:WGLMakie, [joinpath(@__DIR__, "wglmakie.js")]) +const WEBGL = Dependency(:WEBGL, [joinpath(@__DIR__, "WEBGL.js")]) + +include("three_plot.jl") +include("serialization.jl") +include("events.jl") +include("particles.jl") +include("lines.jl") +include("meshes.jl") +include("imagelike.jl") +include("display.jl") + +function activate!() + b = WGLBackend() + Makie.register_backend!(b) + Makie.current_backend[] = b + Makie.set_glyph_resolution!(Makie.Low) + return +end + +const TEXTURE_ATLAS_CHANGED = Ref(false) + +function __init__() + # Activate WGLMakie as backend! + activate!() + browser_display = JSServe.BrowserDisplay() in Base.Multimedia.displays + Makie.inline!(!browser_display) + # We need to update the texture atlas whenever it changes! + # We do this in three_plot! + Makie.font_render_callback!() do sd, uv + TEXTURE_ATLAS_CHANGED[] = true + end +end + +for name in names(Makie) + if name !== :Button && name !== :Slider + @eval import Makie: $(name) + @eval export $(name) + end +end +export inline! + +end # module diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl new file mode 100644 index 00000000000..5c3f7c4c901 --- /dev/null +++ b/WGLMakie/src/display.jl @@ -0,0 +1,134 @@ + +function JSServe.jsrender(session::Session, scene::Scene) + Makie.update!(scene) + three, canvas = WGLMakie.three_display(session, scene) + Makie.push_screen!(scene, three) + return canvas +end + +function JSServe.jsrender(session::Session, scene::Makie.FigureLike) + return JSServe.jsrender(session, Makie.get_scene(scene)) +end + +const WEB_MIMES = (MIME"text/html", MIME"application/vnd.webio.application+html", + MIME"application/prs.juno.plotpane+html", MIME"juliavscode/html") + +for M in WEB_MIMES + @eval begin + function Makie.backend_show(::WGLBackend, io::IO, m::$M, scene::Scene) + three = nothing + inline_display = App() do session::Session + three, canvas = three_display(session, scene) + Makie.push_screen!(scene, three) + return canvas + end + Base.show(io, m, inline_display) + return three + end + end +end + +function scene2image(scene::Scene) + if !JSServe.has_html_display() + error(""" + There is no Display that can show HTML. + If in the REPL and you have a browser, + you can always run `JSServe.browser_display()` to show plots in the browser + """) + end + three = nothing + session = nothing + app = App() do s::Session + session = s + three, canvas = three_display(s, scene) + return canvas + end + # display in current + display(app) + done = Base.timedwait(()-> isready(session.js_fully_loaded), 30.0) + if done == :timed_out + error("JS Session not ready after 30s waiting, possibly errored while displaying") + end + return Makie.colorbuffer(three) +end + +function Makie.backend_show(::WGLBackend, io::IO, m::MIME"image/png", + scene::Scene) + img = scene2image(scene) + return FileIO.save(FileIO.Stream(FileIO.format"PNG", io), img) +end + +function Makie.backend_show(::WGLBackend, io::IO, m::MIME"image/jpeg", + scene::Scene) + img = scene2image(scene) + return FileIO.save(FileIO.Stream(FileIO.format"JPEG", io), img) +end + +function Makie.backend_showable(::WGLBackend, ::T, scene::Scene) where {T<:MIME} + return T in WEB_MIMES +end + +struct WebDisplay <: Makie.AbstractScreen + three::Base.RefValue{Any} + display::Any +end + +function Makie.backend_display(::WGLBackend, scene::Scene) + # Reference to three object which gets set once we serve this to a browser + three_ref = Base.RefValue{Any}(nothing) + app = App() do s, request + three, canvas = three_display(s, scene) + three_ref[] = three + return canvas + end + actual_display = display(app) + return WebDisplay(three_ref, actual_display) +end + +function Base.delete!(td::WebDisplay, scene::Scene, plot::AbstractPlot) + delete!(get_three(td), scene, plot) +end + +function session2image(sessionlike) + s = JSServe.session(sessionlike) + to_data = js"document.querySelector('canvas').toDataURL()" + picture_base64 = JSServe.evaljs_value(s, to_data; time_out=100) + picture_base64 = replace(picture_base64, "data:image/png;base64," => "") + bytes = JSServe.Base64.base64decode(picture_base64) + return ImageMagick.load_(bytes) +end + +function Makie.colorbuffer(screen::ThreeDisplay) + return session2image(screen) +end + +function get_three(screen::WebDisplay; timeout = 30) + # WebDisplay is not guaranteed to get displayed in the browser, so we wait a while + # to see if anything gets displayed! + tstart = time() + while time() - tstart < timeout + if screen.three[] !== nothing + three = screen.three[] + session = JSServe.session(three) + if isready(session.js_fully_loaded) + # Error on js during init! We can't continue like this :'( + if session.init_error[] !== nothing + throw(session.init_error[]) + end + return three + end + end + yield() + end + return nothing +end + +function Makie.colorbuffer(screen::WebDisplay) + return session2image(get_three(screen)) +end + +function Base.insert!(td::WebDisplay, scene::Scene, plot::AbstractPlot) + disp = get_three(td) + disp === nothing && error("Plot needs to be displayed to insert additional plots") + insert!(disp, scene, plot) +end diff --git a/WGLMakie/src/events.jl b/WGLMakie/src/events.jl new file mode 100644 index 00000000000..c90a11853fa --- /dev/null +++ b/WGLMakie/src/events.jl @@ -0,0 +1,111 @@ +macro handle(accessor, body) + obj, field = accessor.args + key = string(field.value) + efield = esc(field.value) + obj = esc(obj) + return quote + if haskey($(obj), $(key)) + $(efield) = $(obj)[$(key)] + $(esc(body)) + return nothing + end + end +end + +function code_to_keyboard(code::String) + if length(code) == 1 && isnumeric(code[1]) + return getfield(Keyboard, Symbol("_" * code)) + end + button = lowercase(code) + if startswith(button, "arrow") + return getfield(Keyboard, Symbol(button[6:end])) + end + if startswith(button, "digit") + return getfield(Keyboard, Symbol("_" * button[6:end])) + end + if startswith(button, "key") + return getfield(Keyboard, Symbol(button[4:end])) + end + button = replace(button, r"(.*)left" => s"left_\1") + button = replace(button, r"(.*)right" => s"right_\1") + sym = Symbol(button) + return if isdefined(Keyboard, sym) + return getfield(Keyboard, sym) + elseif sym == :backquote + return Keyboard.grave_accent + elseif sym == :pageup + return Keyboard.page_up + elseif sym == :pagedown + return Keyboard.page_down + elseif sym == :end + return Keyboard._end + elseif sym == :capslock + return Keyboard.caps_lock + elseif sym == :contextmenu + return Keyboard.menu + else + return Keyboard.unknown + end +end + +function connect_scene_events!(scene::Scene, comm::Observable) + e = events(scene) + on(comm) do msg + @handle msg.mouseposition begin + x, y = Float64.((mouseposition...,)) + e.mouseposition[] = (x, size(scene)[2] - y) + end + @handle msg.mousedown begin + # This can probably be done better from the JS side? + state = e.mousebuttonstate + if mousedown & 1 != 0 && !(Mouse.left in state) + setindex!(e.mousebutton, MouseButtonEvent(Mouse.left, Mouse.press)) + end + if mousedown & 2 != 0 && !(Mouse.right in state) + setindex!(e.mousebutton, MouseButtonEvent(Mouse.right, Mouse.press)) + end + if mousedown & 4 != 0 && !(Mouse.middle in state) + setindex!(e.mousebutton, MouseButtonEvent(Mouse.middle, Mouse.press)) + end + end + @handle msg.mouseup begin + state = e.mousebuttonstate + if mouseup & 1 == 0 && (Mouse.left in state) + setindex!(e.mousebutton, MouseButtonEvent(Mouse.left, Mouse.release)) + end + if mouseup & 2 == 0 && (Mouse.right in state) + setindex!(e.mousebutton, MouseButtonEvent(Mouse.right, Mouse.release)) + end + if mouseup & 4 == 0 && (Mouse.middle in state) + setindex!(e.mousebutton, MouseButtonEvent(Mouse.middle, Mouse.release)) + end + end + @handle msg.scroll begin + e.scroll[] = Float64.((sign.(scroll)...,)) + end + @handle msg.keydown begin + button = code_to_keyboard(keydown) + # don't add unknown buttons...we can't work with them + # and they won't get removed + if button != Keyboard.unknown + e.keyboardbutton[] = KeyEvent(button, Keyboard.press) + end + end + @handle msg.keyup begin + if keyup == "delete_keys" + # this works fine + for key in e.keyboardstate + e.keyboardbutton[] = KeyEvent(key, Keyboard.release) + end + else + e.keyboardbutton[] = KeyEvent(code_to_keyboard(keyup), Keyboard.release) + end + end + return + end + return +end + +function Makie.pick(scene::Scene, THREE::ThreeDisplay, xy::Vec{2,Float64}) + return @warn "Picking not supported yet by WGLMakie" +end diff --git a/WGLMakie/src/imagelike.jl b/WGLMakie/src/imagelike.jl new file mode 100644 index 00000000000..7e45d90487d --- /dev/null +++ b/WGLMakie/src/imagelike.jl @@ -0,0 +1,143 @@ +using Makie: el32convert, surface_normals, get_dim + +# Somehow we started using Nothing for some colors in Makie, +# but the convert leaves them at nothing -.- +# TODO clean this up in Makie +nothing_or_color(c) = to_color(c) +nothing_or_color(c::Nothing) = RGBAf0(0, 0, 0, 1) + +function draw_mesh(mscene::Scene, mesh, plot; uniforms...) + uniforms = Dict(uniforms) + + colormap = if haskey(plot, :colormap) + cmap = lift(el32convert ∘ to_colormap, plot.colormap) + uniforms[:colormap] = Sampler(cmap) + end + + colorrange = if haskey(plot, :colorrange) + uniforms[:colorrange] = lift(Vec2f0, plot.colorrange) + end + + get!(uniforms, :colormap, false) + get!(uniforms, :colorrange, false) + get!(uniforms, :color, false) + get!(uniforms, :model, plot.model) + + uniforms[:normalmatrix] = map(mscene.camera.view, plot.model) do v, m + i = SOneTo(3) + return transpose(inv(v[i, i] * m[i, i])) + end + return Program(WebGL(), lasset("mesh.vert"), lasset("mesh.frag"), mesh; uniforms...) +end + +_length(x::Makie.IntervalSets.AbstractInterval) = 2 +_length(x) = length(x) + +function limits_to_uvmesh(plot) + px, py, pz = plot[1], plot[2], plot[3] + # Special path for ranges of length 2 wich + # can be displayed as a rectangle + if _length(px[]) == 2 && _length(py[]) == 2 + rect = lift(px, py) do x, y + xmin, xmax = extrema(x) + ymin, ymax = extrema(y) + return Rect2D(xmin, ymin, xmax - xmin, ymax - ymin) + end + positions = Buffer(lift(rect-> decompose(Point2f0, rect), rect)) + faces = Buffer(lift(rect -> decompose(GLTriangleFace, rect), rect)) + uv = Buffer(lift(decompose_uv, rect)) + else + function grid(x, y, z, trans) + g = map(CartesianIndices((length(x), length(y)))) do i + return Point3f0(get_dim(x, i, 1, size(z)), get_dim(y, i, 2, size(z)), 0.0) + end + return apply_transform(trans, vec(g)) + end + rect = lift(z -> Tesselation(Rect2D(0f0, 0f0, 1f0, 1f0), size(z) .+ 1), pz) + positions = Buffer(lift(grid, px, py, pz, transform_func_obs(plot))) + faces = Buffer(lift(r -> decompose(GLTriangleFace, r), rect)) + uv = Buffer(lift(decompose_uv, rect)) + end + + vertices = GeometryBasics.meta(positions; uv=uv) + + return GeometryBasics.Mesh(vertices, faces) +end + +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] + function grid(x, y, z) + g = map(CartesianIndices(z)) do i + return Point3f0(get_dim(x, i, 1, size(z)), get_dim(y, i, 2, size(z)), z[i]) + end + return vec(g) + end + positions = Buffer(lift(grid, px, py, pz)) + rect = lift(z -> Tesselation(Rect2D(0f0, 0f0, 1f0, 1f0), size(z)), pz) + faces = Buffer(lift(r -> decompose(GLTriangleFace, r), rect)) + uv = Buffer(lift(decompose_uv, rect)) + pcolor = if haskey(plot, :color) && plot.color[] isa AbstractArray + plot.color + else + pz + end + minfilter = to_value(get(plot, :interpolate, false)) ? :linear : :nearest + color = Sampler(lift(x -> el32convert(x'), pcolor), minfilter=minfilter) + normals = Buffer(lift(surface_normals, px, py, pz)) + vertices = GeometryBasics.meta(positions; uv=uv, normals=normals) + mesh = GeometryBasics.Mesh(vertices, faces) + return draw_mesh(mscene, mesh, plot; uniform_color=color, color=Vec4f0(0), + shading=plot.shading, ambient=plot.ambient, diffuse=plot.diffuse, + specular=plot.specular, shininess=plot.shininess, + lightposition=Vec3f0(1), + highclip=lift(nothing_or_color, plot.highclip), + lowclip=lift(nothing_or_color, plot.lowclip), + nan_color=lift(nothing_or_color, plot.nan_color)) +end + +function create_shader(mscene::Scene, plot::Union{Heatmap,Image}) + image = plot[3] + color = Sampler(map(x -> el32convert(x'), image); + minfilter=to_value(get(plot, :interpolate, false)) ? :linear : :nearest) + mesh = limits_to_uvmesh(plot) + + return draw_mesh(mscene, mesh, plot; uniform_color=color, color=Vec4f0(0), + normals=Vec3f0(0), shading=false, ambient=plot.ambient, + diffuse=plot.diffuse, specular=plot.specular, + colorrange=haskey(plot, :colorrange) ? plot.colorrange : false, + shininess=plot.shininess, lightposition=Vec3f0(1), + highclip=lift(nothing_or_color, plot.highclip), + lowclip=lift(nothing_or_color, plot.lowclip), + nan_color=lift(nothing_or_color, plot.nan_color)) +end + +function create_shader(mscene::Scene, plot::Volume) + x, y, z, vol = plot[1], plot[2], plot[3], plot[4] + box = GeometryBasics.mesh(FRect3D(Vec3f0(0), Vec3f0(1))) + cam = cameracontrols(mscene) + model2 = lift(plot.model, x, y, z) do m, xyz... + mi = minimum.(xyz) + maxi = maximum.(xyz) + w = maxi .- mi + m2 = Mat4f0(w[1], 0, 0, 0, 0, w[2], 0, 0, 0, 0, w[3], 0, mi[1], mi[2], mi[3], 1) + return convert(Mat4f0, m) * m2 + end + + modelinv = lift(inv, model2) + algorithm = lift(x -> Cuint(convert_attribute(x, key"algorithm"())), plot.algorithm) + + return Program(WebGL(), lasset("volume.vert"), lasset("volume.frag"), box, + volumedata=Sampler(lift(Makie.el32convert, vol)), + modelinv=modelinv, colormap=Sampler(lift(to_colormap, plot.colormap)), + colorrange=lift(Vec2f0, plot.colorrange), + isovalue=lift(Float32, plot.isovalue), + isorange=lift(Float32, plot.isorange), + absorption=lift(Float32, get(plot, :absorption, Observable(1f0))), + algorithm=algorithm, ambient=plot.ambient, + diffuse=plot.diffuse, specular=plot.specular, shininess=plot.shininess, + model=model2, + # these get filled in later by serialization, but we need them + # as dummy values here, so that the correct uniforms are emitted + lightposition=Vec3f0(1), eyeposition=Vec3f0(1)) +end diff --git a/WGLMakie/src/lines.jl b/WGLMakie/src/lines.jl new file mode 100644 index 00000000000..549d55046ec --- /dev/null +++ b/WGLMakie/src/lines.jl @@ -0,0 +1,66 @@ +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)) do points, trans + points = apply_transform(trans, topoint(points)) + if plot isa LineSegments + return points + else + # Repeat every second point to connect the lines ! + return topoint(TupleView{2, 1}(points)) + 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}() + for k in (:linewidth, :color) + attribute = lift(plot[k]) do x + x = convert_attribute(x, Key{k}(), key"lines"()) + if plot isa LineSegments + return x + else + # Repeat every second point to connect the lines! + return isscalar(x) ? x : reinterpret(eltype(x), TupleView{2, 1}(x)) + end + end + if isscalar(attribute) + uniforms[k] = attribute + uniforms[Symbol("$(k)_start")] = attribute + uniforms[Symbol("$(k)_end")] = attribute + else + if attribute[] isa AbstractVector{<:Number} && haskey(plot, :colorrange) + attribute = lift(array2color, attribute, plot.colormap, plot.colorrange) + end + per_instance[Symbol("$(k)_start")] = Buffer(lift(x -> x[startr[]], attribute)) + per_instance[Symbol("$(k)_end")] = Buffer(lift(x -> x[endr[]], attribute)) + end + end + + uniforms[:resolution] = scene.camera.resolution + uniforms[:model] = plot.model + positions = meta(Point2f0[(0, -1), (0, 1), (1, -1), (1, 1)], + uv=Vec2f0[(0, 0), (0, 0), (0, 0), (0, 0)]) + instance = GeometryBasics.Mesh(positions, GLTriangleFace[(1, 2, 3), (2, 4, 3)]) + return InstancedProgram(WebGL(), lasset("line_segments.vert"), + lasset("line_segments.frag"), instance, + VertexArray(; per_instance...); uniforms...) +end diff --git a/WGLMakie/src/meshes.jl b/WGLMakie/src/meshes.jl new file mode 100644 index 00000000000..7bb1ef4ced6 --- /dev/null +++ b/WGLMakie/src/meshes.jl @@ -0,0 +1,117 @@ +function vertexbuffer(x, trans) + pos = decompose(Point, x) + return apply_transform(trans, pos) +end + +function vertexbuffer(x::Observable, p) + return Buffer(lift(vertexbuffer, x, transform_func_obs(p))) +end + +facebuffer(x) = facebuffer(GeometryBasics.faces(x)) +facebuffer(x::Observable) = Buffer(lift(facebuffer, x)) +function facebuffer(x::AbstractArray{GLTriangleFace}) + return x +end + +function array2color(colors, cmap, crange) + cmap = RGBAf0.(Colors.color.(to_colormap(cmap)), 1.0) + return Makie.interpolated_getindex.((cmap,), colors, (crange,)) +end + +function array2color(colors::AbstractArray{<:Colorant}, cmap, crange) + return RGBAf0.(colors) +end + +function converted_attribute(plot::AbstractPlot, key::Symbol) + return lift(plot[key]) do value + return convert_attribute(value, Key{key}(), Key{plotkey(plot)}()) + end +end + +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) + data = mattributes(mesh_signal[]) + + uniforms = Dict{Symbol,Any}() + attributes = Dict{Symbol,Any}() + + for (key, default) in (:uv => Vec2f0(0), :normals => Vec3f0(0)) + if haskey(data, key) + attributes[key] = Buffer(get_attribute(mesh_signal, key)) + else + uniforms[key] = Observable(default) + end + end + + if haskey(data, :attributes) && data[:attributes] isa AbstractVector + attr = get_attribute(mesh_signal, :attributes) + attr_id = get_attribute(mesh_signal, :attribute_id) + color = lift((c, id) -> c[Int.(id) .+ 1], attr_id) + attributes[:color] = Buffer(color) + uniforms[:uniform_color] = false + else + color_signal = converted_attribute(plot, :color) + color = color_signal[] + mesh_color = color_signal[] + uniforms[:uniform_color] = Observable(false) # this is the default + + if color isa Colorant && haskey(data, :color) + color_signal = get_attribute(mesh_signal, :color) + color = color_signal[] + end + + if color isa AbstractArray + c_converted = if color isa AbstractArray{<:Colorant} + color_signal + elseif color isa AbstractArray{<:Number} + lift(array2color, color_signal, plot.colormap, plot.colorrange) + else + error("Unsupported color type: $(typeof(color))") + end + if c_converted[] isa AbstractVector + attributes[:color] = Buffer(c_converted) # per vertex colors + else + uniforms[:uniform_color] = Sampler(c_converted) # Texture + !haskey(attributes, :uv) && + @warn "Mesh doesn't use Texturecoordinates, but has a Texture. Colors won't map" + end + elseif color isa Colorant && !haskey(attributes, :color) + uniforms[:uniform_color] = color_signal + else + error("Unsupported color type: $(typeof(color))") + end + end + + if !haskey(attributes, :color) + uniforms[:color] = Vec4f0(0) # make sure we have a color attribute + end + + uniforms[:shading] = plot.shading + + for key in (:ambient, :diffuse, :specular, :shininess) + uniforms[key] = plot[key] + end + + faces = facebuffer(mesh_signal) + positions = vertexbuffer(mesh_signal, plot) + instance = GeometryBasics.Mesh(GeometryBasics.meta(positions; attributes...), faces) + + get!(uniforms, :colorrange, true) + get!(uniforms, :colormap, true) + get!(uniforms, :model, plot.model) + get!(uniforms, :lightposition, Vec3f0(1)) + + get!(uniforms, :nan_color, RGBAf0(0, 0, 0, 0)) + get!(uniforms, :highclip, RGBAf0(0, 0, 0, 0)) + get!(uniforms, :lowclip, RGBAf0(0, 0, 0, 0)) + + uniforms[:normalmatrix] = map(scene.camera.view, plot.model) do v, m + i = SOneTo(3) + return transpose(inv(v[i, i] * m[i, i])) + end + + return Program(WebGL(), lasset("mesh.vert"), lasset("mesh.frag"), instance; uniforms...) +end diff --git a/WGLMakie/src/particles.jl b/WGLMakie/src/particles.jl new file mode 100644 index 00000000000..0a1a80fcda4 --- /dev/null +++ b/WGLMakie/src/particles.jl @@ -0,0 +1,221 @@ + +function handle_color!(uniform_dict, instance_dict) + color, udict = if haskey(uniform_dict, :color) + to_value(uniform_dict[:color]), uniform_dict + elseif haskey(instance_dict, :color) + to_value(instance_dict[:color]), instance_dict + else + nothing, uniform_dict + end + if color isa Colorant || + color isa AbstractVector{<:Colorant} || + color === nothing + delete!(uniform_dict, :colormap) + elseif color isa AbstractArray{<:Real} + udict[:color] = lift(x -> convert(Vector{Float32}, x), udict[:color]) + # udict[:color] = lift(x -> convert(Vector{Float32}, x), udict[:color]) + uniform_dict[:color_getter] = """ + vec4 get_color(){ + vec2 norm = get_colorrange(); + float normed = (color - norm.x) / (norm.y - norm.x); + return texture(colormap, vec2(normed, 0)); + } + """ + end +end + +const IGNORE_KEYS = Set([:shading, :overdraw, :rotation, :distancefield, :markerspace, + :fxaa, :visible, :transformation, :alpha, :linewidth, + :transparency, :marker, :lightposition, :cycle]) + +function create_shader(scene::Scene, plot::MeshScatter) + # Potentially per instance attributes + per_instance_keys = (:rotations, :markersize, :color, :intensity) + per_instance = filter(plot.attributes.attributes) do (k, v) + return k in per_instance_keys && !(isscalar(v[])) + end + per_instance[:offset] = apply_transform(transform_func_obs(plot), plot[1]) + + for (k, v) in per_instance + per_instance[k] = Buffer(lift_convert(k, v, plot)) + end + + uniforms = filter(plot.attributes.attributes) do (k, v) + return (!haskey(per_instance, k)) && isscalar(v[]) + end + + uniform_dict = Dict{Symbol,Any}() + for (k, v) in uniforms + k in IGNORE_KEYS && continue + uniform_dict[k] = lift_convert(k, v, plot) + end + + handle_color!(uniform_dict, per_instance) + instance = convert_attribute(plot.marker[], key"marker"(), key"meshscatter"()) + + if !hasproperty(instance, :uv) + uniform_dict[:uv] = Vec2f0(0) + end + + return InstancedProgram(WebGL(), lasset("particles.vert"), lasset("particles.frag"), + instance, VertexArray(; per_instance...); uniform_dict...) +end + +@enum Shape CIRCLE RECTANGLE ROUNDED_RECTANGLE DISTANCEFIELD TRIANGLE + +primitive_shape(::Union{String,Char,Vector{Char}}) = Cint(DISTANCEFIELD) +primitive_shape(x::X) where {X} = Cint(primitive_shape(X)) +primitive_shape(::Type{<:Circle}) = Cint(CIRCLE) +primitive_shape(::Type{<:Rect2D}) = Cint(RECTANGLE) +primitive_shape(::Type{T}) where {T} = error("Type $(T) not supported") +primitive_shape(x::Shape) = Cint(x) + +using Makie: to_spritemarker + +function scatter_shader(scene::Scene, attributes) + # Potentially per instance attributes + per_instance_keys = (:offset, :rotations, :markersize, :color, :intensity, + :uv_offset_width, :marker_offset) + uniform_dict = Dict{Symbol,Any}() + if haskey(attributes, :marker) && attributes[:marker][] isa Union{Vector{Char},String} + x = pop!(attributes, :marker) + attributes[:uv_offset_width] = lift(x -> Makie.glyph_uv_width!.(collect(x)), + x) + uniform_dict[:shape_type] = Cint(3) + end + + per_instance = filter(attributes) do (k, v) + return k in per_instance_keys && !(isscalar(v[])) + end + + for (k, v) in per_instance + per_instance[k] = Buffer(lift_convert(k, v, nothing)) + end + + uniforms = filter(attributes) do (k, v) + return !haskey(per_instance, k) + end + + for (k, v) in uniforms + k in IGNORE_KEYS && continue + uniform_dict[k] = lift_convert(k, v, nothing) + end + + get!(uniform_dict, :shape_type) do + return lift(x -> primitive_shape(to_spritemarker(x)), attributes[:marker]) + end + if uniform_dict[:shape_type][] == 3 + atlas = Makie.get_texture_atlas() + uniform_dict[:distancefield] = Sampler(atlas.data, minfilter=:linear, + magfilter=:linear, anisotropic=16f0) + uniform_dict[:atlas_texture_size] = Float32(size(atlas.data, 1)) # Texture must be quadratic + else + uniform_dict[:atlas_texture_size] = 0f0 + uniform_dict[:distancefield] = Observable(false) + end + + if !haskey(per_instance, :uv_offset_width) + get!(uniform_dict, :uv_offset_width) do + return if haskey(attributes, :marker) && + to_spritemarker(attributes[:marker][]) isa Char + lift(x -> Makie.glyph_uv_width!(to_spritemarker(x)), + attributes[:marker]) + else + Vec4f0(0) + end + end + end + + space = get(uniforms, :markerspace, Observable(SceneSpace)) + uniform_dict[:use_pixel_marker] = map(space) do space + return space == Pixel + end + handle_color!(uniform_dict, per_instance) + + instance = uv_mesh(Rect2D(-0.5f0, -0.5f0, 1f0, 1f0)) + uniform_dict[:resolution] = scene.camera.resolution + return InstancedProgram(WebGL(), lasset("simple.vert"), lasset("sprites.frag"), + instance, VertexArray(; per_instance...); uniform_dict...) +end + +function create_shader(scene::Scene, plot::Scatter) + # Potentially per instance attributes + per_instance_keys = (:offset, :rotations, :markersize, :color, :intensity, + :marker_offset) + per_instance = filter(plot.attributes.attributes) do (k, v) + return k in per_instance_keys && !(isscalar(v[])) + end + attributes = copy(plot.attributes.attributes) + attributes[:offset] = apply_transform(transform_func_obs(plot), plot[1]) + attributes[:billboard] = map(rot -> isa(rot, Billboard), plot.rotations) + attributes[:pixelspace] = getfield(scene.camera, :pixel_space) + attributes[:model] = plot.model + attributes[:markerspace] = plot.markerspace + delete!(attributes, :uv_offset_width) + return scatter_shader(scene, attributes) +end + +value_or_first(x::AbstractArray) = first(x) +value_or_first(x::StaticArray) = x +value_or_first(x) = x + +function create_shader(scene::Scene, plot::Makie.Text) + + string_obs = plot[1] + liftkeys = (:position, :textsize, :font, :align, :rotation, :model, :justification, :lineheight, :space, :offset) + + args = getindex.(Ref(plot), liftkeys) + + gl_text = lift(string_obs, scene.camera.projectionview, Makie.transform_func_obs(scene), args...) do str, projview, transfunc, pos, tsize, font, align, rotation, model, j, l, space, offset + # For annotations, only str (x[1]) will get updated, but all others are updated too! + args = @get_attribute plot (position, textsize, font, align, rotation, offset) + res = Vec2f0(widths(pixelarea(scene)[])) + return Makie.preprojected_glyph_arrays(str, pos, plot._glyphlayout[], font, textsize, space, projview, res, offset, transfunc) + end + + # unpack values from the one signal: + positions, offset, uv_offset_width, scale = map((1, 2, 3, 4)) do i + lift(getindex, gl_text, i) + end + + atlas = get_texture_atlas() + keys = (:color, :rotation) + + signals = map(keys) do key + return lift(positions, plot[key]) do pos, attr + str = string_obs[] + if str isa AbstractVector + if isempty(str) + attr = convert_attribute(value_or_first(attr), Key{key}()) + return Vector{typeof(attr)}() + else + result = [] + broadcast_foreach(str, attr) do st, aa + for att in attribute_per_char(st, aa) + push!(result, convert_attribute(att, Key{key}())) + end + end + # narrow the type from any, this is ugly + return identity.(result) + end + else + return Makie.get_attribute(plot, key) + end + end + end + uniforms = Dict( + :model => plot.model, + :shape_type => Observable(Cint(3)), + :color => signals[1], + :rotations => signals[2], + :markersize => scale, + :markerspace => Observable(Pixel), + :marker_offset => offset, + :offset => positions, + :uv_offset_width => uv_offset_width, + :transform_marker => Observable(false), + :billboard => Observable(false), + :pixelspace => getfield(scene.camera, :pixel_space)) + + return scatter_shader(scene, uniforms) +end diff --git a/WGLMakie/src/serialization.jl b/WGLMakie/src/serialization.jl new file mode 100644 index 00000000000..47b50abde09 --- /dev/null +++ b/WGLMakie/src/serialization.jl @@ -0,0 +1,329 @@ +using Colors +using ShaderAbstractions: InstancedProgram, Program +using Makie: Key, plotkey +using Colors: N0f8 + +Makie.plotkey(::Nothing) = :scatter + +function lift_convert(key, value, plot) + val = lift(value) do value + return wgl_convert(value, Key{key}(), Key{plotkey(plot)}()) + end + if key == :colormap && val[] isa AbstractArray + return ShaderAbstractions.Sampler(val) + else + return val + end +end + +function Base.pairs(mesh::GeometryBasics.Mesh) + return (kv for kv in GeometryBasics.attributes(mesh)) +end + +function GeometryBasics.faces(x::VertexArray) + return GeometryBasics.faces(getfield(x, :data)) +end + +tlength(T) = length(T) +tlength(::Type{<:Real}) = 1 + +serialize_three(val::Number) = val +serialize_three(val::Vec2f0) = convert(Vector{Float32}, val) +serialize_three(val::Vec3f0) = convert(Vector{Float32}, val) +serialize_three(val::Vec4f0) = convert(Vector{Float32}, val) +serialize_three(val::Quaternion) = convert(Vector{Float32}, collect(val.data)) +serialize_three(val::RGB) = Float32[red(val), green(val), blue(val)] +serialize_three(val::RGBA) = Float32[red(val), green(val), blue(val), alpha(val)] +serialize_three(val::Mat4f0) = vec(val) +serialize_three(val::Mat3) = vec(val) + +function serialize_three(observable::Observable) + return Dict(:type => "Observable", :id => observable.id, + :value => serialize_three(observable[])) +end + +function serialize_three(array::AbstractArray) + return serialize_three(flatten_buffer(array)) +end + +function serialize_three(array::Buffer) + return serialize_three(flatten_buffer(array)) +end + +function serialize_three(array::AbstractArray{UInt8}) + return Dict(:type => "Uint8Array", :data => array) +end + +function serialize_three(array::AbstractArray{Int32}) + return Dict(:type => "Int32Array", :data => array) +end + +function serialize_three(array::AbstractArray{UInt32}) + return Dict(:type => "Uint32Array", :data => array) +end + +function serialize_three(array::AbstractArray{Float32}) + return Dict(:type => "Float32Array", :data => array) +end + +function serialize_three(array::AbstractArray{Float16}) + return Dict(:type => "Float32Array", :data => array) +end + +function serialize_three(array::AbstractArray{Float64}) + return Dict(:type => "Float64Array", :data => array) +end + +function serialize_three(color::Sampler{T,N}) where {T,N} + tex = Dict(:type => "Sampler", :data => serialize_three(color.data), + :size => [size(color.data)...], :three_format => three_format(T), + :three_type => three_type(eltype(T)), + :minFilter => three_filter(color.minfilter), + :magFilter => three_filter(color.magfilter), + :wrapS => three_repeat(color.repeat[1]), :anisotropy => color.anisotropic) + if N > 1 + tex[:wrapT] = three_repeat(color.repeat[2]) + end + if N > 2 + tex[:wrapR] = three_repeat(color.repeat[3]) + end + return tex +end + +function serialize_uniforms(dict::Dict) + result = Dict{Symbol,Any}() + for (k, v) in dict + result[k] = serialize_three(to_value(v)) + end + return result +end + +three_format(::Type{<:Real}) = "RedFormat" +three_format(::Type{<:RGB}) = "RGBFormat" +three_format(::Type{<:RGBA}) = "RGBAFormat" + +three_type(::Type{Float16}) = "FloatType" +three_type(::Type{Float32}) = "FloatType" +three_type(::Type{N0f8}) = "UnsignedByteType" + +function three_filter(sym) + sym == :linear && return "LinearFilter" + return sym == :nearest && return "NearestFilter" +end + +function three_repeat(s::Symbol) + s == :clamp_to_edge && return "ClampToEdgeWrapping" + s == :mirrored_repeat && return "MirroredRepeatWrapping" + return s == :repeat && return "RepeatWrapping" +end + +""" + flatten_buffer(array::AbstractArray) +Flattens `array` array to be a 1D Vector of Float32 / UInt8. +If presented with AbstractArray{<: Colorant/Tuple/SVector}, it will flatten those +to their element type. +""" +function flatten_buffer(array::AbstractArray{<: Number}) + return array +end + +function flatten_buffer(array::Buffer) + return flatten_buffer(getfield(array, :data)) +end + +function flatten_buffer(array::AbstractArray{T}) where {T<:N0f8} + return reinterpret(UInt8, array) +end + +function flatten_buffer(array::AbstractArray{T}) where {T} + return flatten_buffer(reinterpret(eltype(T), array)) +end + +lasset(paths...) = read(joinpath(@__DIR__, "..", "assets", paths...), String) + +isscalar(x::StaticArrays.StaticArray) = true +isscalar(x::AbstractArray) = false +isscalar(x::Billboard) = isscalar(x.rotation) +isscalar(x::Observable) = isscalar(x[]) +isscalar(x) = true + +function ShaderAbstractions.type_string(::ShaderAbstractions.AbstractContext, + ::Type{<:Makie.Quaternion}) + return "vec4" +end + +function ShaderAbstractions.convert_uniform(::ShaderAbstractions.AbstractContext, + t::Quaternion) + return convert(Quaternion, t) +end + +function wgl_convert(value, key1, key2) + val = Makie.convert_attribute(value, key1, key2) + return if val isa AbstractArray{<:Float64} + return Makie.el32convert(val) + else + return val + end +end + +function wgl_convert(value::AbstractMatrix, ::key"colormap", key2) + return ShaderAbstractions.Sampler(value) +end + +function serialize_buffer_attribute(buffer::AbstractVector{T}) where {T} + return Dict(:flat => serialize_three(buffer), :type_length => tlength(T)) +end + +function serialize_named_buffer(buffer) + return Dict(map(pairs(buffer)) do (name, buff) + return name => serialize_buffer_attribute(buff) + end) +end + +function register_geometry_updates(update_buffer::Observable, named_buffers) + for (name, buffer) in pairs(named_buffers) + if buffer isa Buffer + on(ShaderAbstractions.updater(buffer).update) do (f, args) + # update to replace the whole buffer! + if f === (setindex!) && args[1] isa AbstractArray && args[2] isa Colon + new_array = args[1] + flat = flatten_buffer(new_array) + update_buffer[] = [name, serialize_three(flat), length(new_array)] + end + return + end + end + end + return update_buffer +end + +function register_geometry_updates(update_buffer::Observable, program::Program) + return register_geometry_updates(update_buffer, program.vertexarray) +end + +function register_geometry_updates(update_buffer::Observable, program::InstancedProgram) + return register_geometry_updates(update_buffer, program.per_instance) +end + +function uniform_updater(uniforms::Dict) + updater = Observable(Any[:none, []]) + for (name, value) in uniforms + if value isa Sampler + on(ShaderAbstractions.updater(value).update) do (f, args) + if args[2] isa Colon && f == setindex! + updater[] = [name, serialize_three(args[1])] + end + return + end + else + value isa Observable || continue + on(value) do value + updater[] = [name, serialize_three(value)] + return + end + end + end + return updater +end + +function serialize_three(ip::InstancedProgram) + program = serialize_three(ip.program) + program[:instance_attributes] = serialize_named_buffer(ip.per_instance) + register_geometry_updates(program[:attribute_updater], ip) + return program +end + +function serialize_three(program::Program) + indices = GeometryBasics.faces(program.vertexarray) + indices = reinterpret(UInt32, indices) + uniforms = serialize_uniforms(program.uniforms) + attribute_updater = Observable(["", [], 0]) + register_geometry_updates(attribute_updater, program) + return Dict(:vertexarrays => serialize_named_buffer(program.vertexarray), + :faces => indices, :uniforms => uniforms, + :vertex_source => program.vertex_source, + :fragment_source => program.fragment_source, + :uniform_updater => uniform_updater(program.uniforms), + :attribute_updater => attribute_updater) +end + +function serialize_scene(scene::Scene, serialized_scenes=[]) + hexcolor(c) = "#" * hex(Colors.color(to_color(c))) + pixel_area = lift(area -> [minimum(area)..., widths(area)...], pixelarea(scene)) + cam_controls = cameracontrols(scene) + cam3d_state = if cam_controls isa Camera3D + fields = (:lookat, :upvector, :eyeposition, :fov, :near, :far) + Dict((f => serialize_three(getfield(cam_controls, f)[]) for f in fields)) + else + nothing + end + serialized = Dict(:pixelarea => pixel_area, + :backgroundcolor => lift(hexcolor, scene.backgroundcolor), + :clearscene => scene.clear, + :camera => serialize_camera(scene), + :plots => serialize_plots(scene, scene.plots), + :cam3d_state => cam3d_state, + :visible => scene.visible, + :uuid => js_uuid(scene)) + + push!(serialized_scenes, serialized) + foreach(child -> serialize_scene(child, serialized_scenes), scene.children) + + return serialized_scenes +end + +function serialize_plots(scene::Scene, plots::Vector{T}, result=[]) where {T<:AbstractPlot} + for plot in plots + # if no plots inserted, this truely is an atomic + if isempty(plot.plots) + plot_data = serialize_three(scene, plot) + plot_data[:uuid] = js_uuid(plot) + push!(result, plot_data) + else + serialize_plots(scene, plot.plots, result) + end + end + return result +end + +function serialize_three(scene::Scene, plot::AbstractPlot) + program = create_shader(scene, plot) + mesh = serialize_three(program) + mesh[:name] = string(Makie.plotkey(plot)) * "-" * string(objectid(plot)) + mesh[:visible] = plot.visible + mesh[:uuid] = js_uuid(plot) + uniforms = mesh[:uniforms] + updater = mesh[:uniform_updater] + + delete!(uniforms, :lightposition) + + if haskey(plot, :lightposition) + eyepos = scene.camera.eyeposition + lightpos = lift(Vec3f0, plot.lightposition, eyepos) do pos, eyepos + return ifelse(pos == :eyeposition, eyepos, pos)::Vec3f0 + end + uniforms[:lightposition] = serialize_three(lightpos[]) + on(lightpos) do value + updater[] = [:lightposition, serialize_three(value)] + return + end + end + if haskey(plot, :space) + mesh[:space] = plot.space[] + end + + return mesh +end + +function serialize_camera(scene::Scene) + cam = scene.camera + return lift(cam.view, cam.projection, cam.resolution) do v, p, res + # projectionview updates with projection & view + pv = cam.projectionview[] + # same goes for eyeposition, since an eyepos change will trigger + # a view matrix change! + ep = cam.eyeposition[] + pixel_space = cam.pixel_space[] + return [serialize_three.((v, p, pv, res, ep, pixel_space))...] + end +end diff --git a/WGLMakie/src/three_plot.jl b/WGLMakie/src/three_plot.jl new file mode 100644 index 00000000000..1056af7d9ef --- /dev/null +++ b/WGLMakie/src/three_plot.jl @@ -0,0 +1,113 @@ +struct ThreeDisplay <: Makie.AbstractScreen + session::JSServe.Session +end + +JSServe.session(td::ThreeDisplay) = td.session + +# We use objectid to find objects on the js side +js_uuid(object) = string(objectid(object)) + +function Base.insert!(td::ThreeDisplay, scene::Scene, plot::AbstractPlot) + plot_data = serialize_plots(scene, [plot]) + WGL.insert_plot(td.session, js_uuid(scene), plot_data) + return +end + +function Base.delete!(td::ThreeDisplay, scene::Scene, plot::AbstractPlot) + uuids = js_uuid.(Makie.flatten_plots(plot)) + WGL.delete_plots(td.session, js_uuid(scene), uuids) + return +end + +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.flatten_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 + +""" + find_plots(td::ThreeDisplay, plot::AbstractPlot) + +Gets the ThreeJS object representing the plot object. +""" +function find_plots(td::ThreeDisplay, plot::AbstractPlot) + return find_plots(JSServe.session(td), plot) +end + +function find_plots(session::Session, plot::AbstractPlot) + uuids = js_uuid.(Makie.flatten_plots(plot)) + return WGL.find_plots(session, uuids) +end + + +function JSServe.print_js_code(io::IO, plot::AbstractPlot, context) + uuids = js_uuid.(Makie.flatten_plots(plot)) + JSServe.print_js_code(io, js"$(WGL).find_plots($(uuids))", context) +end + +function three_display(session::Session, scene::Scene) + serialized = serialize_scene(scene) + + if TEXTURE_ATLAS_CHANGED[] + JSServe.update_cached_value!(session, Makie.get_texture_atlas().data) + TEXTURE_ATLAS_CHANGED[] = false + end + + JSServe.register_resource!(session, serialized) + window_open = scene.events.window_open + + width, height = size(scene) + + canvas = DOM.um("canvas", tabindex="0") + wrapper = DOM.div(canvas) + comm = Observable(Dict{String,Any}()) + push!(session, comm) + + scene_data = Observable(serialized) + + canvas_width = lift(x -> [round.(Int, widths(x))...], pixelarea(scene)) + + scene_id = objectid(scene) + setup = js""" + function setup(scenes){ + const canvas = $(canvas) + + const scene_id = $(scene_id) + const renderer = $(WGL).threejs_module(canvas, $comm, $width, $height) + if ( renderer ) { + const three_scenes = scenes.map(x=> $(WGL).deserialize_scene(x, canvas)) + const cam = new $(THREE).PerspectiveCamera(45, 1, 0, 100) + $(WGL).start_renderloop(renderer, three_scenes, cam) + JSServe.on_update($canvas_width, w_h => { + // `renderer.setSize` correctly updates `canvas` dimensions + const pixelRatio = renderer.getPixelRatio(); + renderer.setSize(w_h[0] / pixelRatio, w_h[1] / pixelRatio); + }) + } else { + const warning = $(WEBGL).getWebGLErrorMessage(); + $(wrapper).removeChild(canvas) + $(wrapper).appendChild(warning) + } + } + """ + + onjs(session, scene_data, setup) + scene_data[] = scene_data[] + connect_scene_events!(scene, comm) + three = ThreeDisplay(session) + + on(session.on_close) do closed + if closed + scene_uuids, plot_uuids = all_plots_scenes(scene) + WGL.delete_scenes(session, scene_uuids, plot_uuids) + window_open[] = false + end + end + + return three, wrapper +end diff --git a/WGLMakie/src/wglmakie.js b/WGLMakie/src/wglmakie.js new file mode 100644 index 00000000000..c9feeaf30fa --- /dev/null +++ b/WGLMakie/src/wglmakie.js @@ -0,0 +1,730 @@ +const WGLMakie = (function () { + const pixelRatio = window.devicePixelRatio || 1.0; + // global scene cache to look them up for dynamic operations in Makie + // e.g. insert!(scene, plot) / delete!(scene, plot) + const scene_cache = {}; + const plot_cache = {}; + + function add_scene(scene_id, three_scene) { + scene_cache[scene_id] = three_scene; + } + + function find_scene(scene_id) { + return scene_cache[scene_id]; + } + + function delete_scene(scene_id) { + const scene = scene_cache[scene_id]; + if (!scene) { + return; + } + while (scene.children.length > 0) { + scene.remove(scene.children[0]); + } + delete scene_cache[scene_id]; + } + + function find_plots(plot_uuids) { + const plots = []; + plot_uuids.forEach((id) => { + const plot = plot_cache[id]; + if (plot) { + plots.push(plot); + } + }); + return plots; + } + + function delete_scenes(scene_uuids, plot_uuids) { + plot_uuids.forEach((plot_id) => { + delete plot_cache[plot_id] + }) + scene_uuids.forEach((scene_id=>{ + delete_scene(scene_id) + })) + } + + function insert_plot(scene_id, plot_data) { + const scene = find_scene(scene_id); + plot_data.forEach(plot=> { + add_plot(scene, plot); + }) + } + + function delete_plots(scene_id, plot_uuids) { + const scene = find_scene(scene_id); + const plots = find_plots(plot_uuids); + plots.forEach((p) => { + scene.remove(p) + delete plot_cache[p] + }); + } + + 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; + if (plot_data.space == "screen") { + plot_data.uniforms.view = new THREE.Uniform(new THREE.Matrix4()); + plot_data.uniforms.projection = cam.pixel_space; + plot_data.uniforms.projectionview = cam.pixel_space; + } else { + plot_data.uniforms.view = cam.view; + plot_data.uniforms.projection = cam.projection; + plot_data.uniforms.projectionview = cam.projectionview; + plot_data.uniforms.eyeposition = cam.eyeposition; + } + plot_data.uniforms.resolution = cam.resolution; + + const p = deserialize_plot(plot_data); + plot_cache[plot_data.uuid] = p; + scene.add(p); + } + + // Taken from https://andreasrohner.at/posts/Web%20Development/JavaScript/Simple-orbital-camera-controls-for-THREE-js/ + function attach_3d_camera(domElement, camera_matrices, cam3d) { + if (cam3d === undefined) { + // we just support 3d cameras atm + return; + } + const w = camera_matrices.resolution.value.x; + const h = camera_matrices.resolution.value.y; + const camera = new THREE.PerspectiveCamera( + cam3d.fov, + w / h, + cam3d.near, + cam3d.far + ); + + const center = new THREE.Vector3(...cam3d.lookat); + camera.up = new THREE.Vector3(...cam3d.upvector); + camera.position.set(...cam3d.eyeposition); + camera.lookAt(center); + + function update() { + camera.updateProjectionMatrix(); + camera.updateWorldMatrix(); + camera_matrices.view.value = camera.matrixWorldInverse; + camera_matrices.projection.value = camera.projectionMatrix; + camera_matrices.eyeposition.value = camera.position; + } + + function addMouseHandler(domObject, drag, zoomIn, zoomOut) { + let startDragX = null; + let startDragY = null; + function mouseWheelHandler(e) { + e = window.event || e; + const delta = Math.sign(e.deltaY); + if (delta == -1) { + zoomOut(); + } else if (delta == 1) { + zoomIn(); + } + + e.preventDefault(); + } + function mouseDownHandler(e) { + startDragX = e.clientX; + startDragY = e.clientY; + + e.preventDefault(); + } + function mouseMoveHandler(e) { + 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) { + 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(domElement, drag, zoomIn, zoomOut); + } + + function create_texture(data) { + const buffer = deserialize_three(data.data); + if (data.size.length == 3) { + const tex = new THREE.DataTexture3D( + buffer, + data.size[0], + data.size[1], + data.size[2] + ); + tex.format = THREE[data.three_format]; + tex.type = THREE[data.three_type]; + return tex; + } else { + return new THREE.DataTexture( + buffer, + data.size[0], + data.size[1], + THREE[data.three_format], + THREE[data.three_type] + ); + } + } + + function convert_texture(data) { + const tex = create_texture(data); + tex.needsUpdate = true; + tex.minFilter = THREE[data.minFilter]; + tex.magFilter = THREE[data.magFilter]; + tex.anisotropy = data.anisotropy; + tex.wrapS = THREE[data.wrapS]; + if (data.size.length > 2) { + tex.wrapT = THREE[data.wrapT]; + } + if (data.size.length > 3) { + tex.wrapR = THREE[data.wrapR]; + } + return tex; + } + + const typed_array_names = [ + "Uint8Array", + "Int32Array", + "Uint32Array", + "Float32Array", + ]; + + function deserialize_three(data) { + if (typeof data === "number") { + return data; + } + + if (typeof data === "boolean") { + return data; + } + + if (typed_array_names.includes(data.constructor.name)) { + return data; + } + + if (data.type !== undefined) { + if (data.type == "Sampler") { + return convert_texture(data); + } + if (typed_array_names.includes(data.type)) { + return new window[data.type](data.data); + } + } + + if (JSServe.is_list(data)) { + if (data.length == 2) { + return new THREE.Vector2().fromArray(data); + } + if (data.length == 3) { + return new THREE.Vector3().fromArray(data); + } + if (data.length == 4) { + return new THREE.Vector4().fromArray(data); + } + if (data.length == 16) { + const mat = new THREE.Matrix4(); + mat.fromArray(data); + return mat; + } + return data; + } + return data; // we just leave data we dont know alone! + } + + function BufferAttribute(buffer) { + const buff = deserialize_three(buffer.flat); + const jsbuff = new THREE.BufferAttribute(buff, buffer.type_length); + jsbuff.setUsage(THREE.DynamicDrawUsage); + return jsbuff; + } + + function InstanceBufferAttribute(buffer) { + const buff = deserialize_three(buffer.flat); + const jsbuff = new THREE.InstancedBufferAttribute( + buff, + buffer.type_length + ); + jsbuff.setUsage(THREE.DynamicDrawUsage); + return jsbuff; + } + + function attach_geometry(buffer_geometry, vertexarrays, faces) { + for (const name in vertexarrays) { + const buffer = BufferAttribute(vertexarrays[name]); + buffer_geometry.setAttribute(name, buffer); + } + buffer_geometry.setIndex(faces); + buffer_geometry.boundingSphere = new THREE.Sphere(); + // don't use intersection / culling + buffer_geometry.boundingSphere.radius = 10000000000000; + buffer_geometry.frustumCulled = false; + return buffer_geometry; + } + + function attach_instanced_geometry(buffer_geometry, instance_attributes) { + for (const name in instance_attributes) { + const buffer = InstanceBufferAttribute(instance_attributes[name]); + buffer_geometry.setAttribute(name, buffer); + } + } + + function recreate_instanced_geometry(mesh) { + const buffer_geometry = new THREE.InstancedBufferGeometry(); + const vertexarrays = {}; + const instance_attributes = {}; + const faces = [...mesh.geometry.index.array]; + Object.keys(mesh.geometry.attributes).forEach((name) => { + const buffer = mesh.geometry.attributes[name]; + // really dont know why copying an array is considered rocket science in JS + const copy = buffer.to_update + ? buffer.to_update + : buffer.array.map((x) => x); + if (buffer.isInstancedBufferAttribute) { + instance_attributes[name] = { + flat: copy, + type_length: buffer.itemSize, + }; + } else { + vertexarrays[name] = { + flat: copy, + type_length: buffer.itemSize, + }; + } + }); + attach_geometry(buffer_geometry, vertexarrays, faces); + attach_instanced_geometry(buffer_geometry, instance_attributes); + mesh.geometry.dispose(); + mesh.geometry = buffer_geometry; + mesh.needsUpdate = true; + } + + function recreate_geometry(mesh, vertexarrays, faces) { + const buffer_geometry = new THREE.BufferGeometry(); + attach_geometry(buffer_geometry, vertexarrays, faces); + mesh.geometry = buffer_geometry; + mesh.needsUpdate = true; + } + + function update_buffer(mesh, buffer) { + const { name, flat, len } = buffer; + const geometry = mesh.geometry; + const jsb = geometry.attributes[name]; + jsb.set(flat, 0); + jsb.needsUpdate = true; + geometry.instanceCount = len; + } + + function deserialize_uniforms(data) { + const result = {}; + for (const name in data) { + const value = data[name]; + // this is already a uniform - happens when we attach additional + // uniforms like the camera matrices in a later stage! + if (value.constructor.name == "Uniform") { + result[name] = value; + } else { + const ser = deserialize_three(value); + result[name] = new THREE.Uniform(ser); + } + } + return result; + } + + function create_material(program) { + const is_volume = "volumedata" in program.uniforms; + return new THREE.RawShaderMaterial({ + uniforms: deserialize_uniforms(program.uniforms), + vertexShader: deserialize_three(program.vertex_source), + fragmentShader: deserialize_three(program.fragment_source), + side: is_volume ? THREE.BackSide : THREE.DoubleSide, + transparent: true, + // depthTest: true, + // depthWrite: true + }); + } + + function create_mesh(program) { + const buffer_geometry = new THREE.BufferGeometry(); + attach_geometry(buffer_geometry, program.vertexarrays, program.faces); + const material = create_material(program); + return new THREE.Mesh(buffer_geometry, material); + } + + function create_instanced_mesh(program) { + const buffer_geometry = new THREE.InstancedBufferGeometry(); + attach_geometry(buffer_geometry, program.vertexarrays, program.faces); + attach_instanced_geometry(buffer_geometry, program.instance_attributes); + const material = create_material(program); + return new THREE.Mesh(buffer_geometry, material); + } + + function deserialize_plot(data) { + let mesh; + if ("instance_attributes" in data) { + mesh = create_instanced_mesh(data); + } else { + mesh = create_mesh(data); + } + mesh.name = data.name; + mesh.frustumCulled = false; + mesh.matrixAutoUpdate = false; + const update_visible = (v) => (mesh.visible = v); + update_visible(JSServe.get_observable(data.visible)); + JSServe.on_update(data.visible, update_visible); + connect_uniforms(mesh, data.uniform_updater); + connect_attributes(mesh, data.attribute_updater); + return mesh; + } + + function deserialize_scene(data, canvas) { + scene = new THREE.Scene(); + add_scene(data.uuid, scene) + scene.frustumCulled = false; + scene.pixelarea = data.pixelarea; + scene.backgroundcolor = data.backgroundcolor; + scene.clearscene = data.clearscene; + + const cam = { + view: new THREE.Uniform(new THREE.Matrix4()), + projection: new THREE.Uniform(new THREE.Matrix4()), + projectionview: new THREE.Uniform(new THREE.Matrix4()), + pixel_space: new THREE.Uniform(new THREE.Matrix4()), + resolution: new THREE.Uniform(new THREE.Vector2()), + eyeposition: new THREE.Uniform(new THREE.Vector3()), + }; + + scene.wgl_camera = cam; + + function update_cam(camera) { + const [ + view, + projection, + projectionview, + resolution, + eyepos, + pixel_space, + ] = camera; + const resolution_scaled = JSServe.deserialize_js(resolution) + cam.view.value.fromArray(view); + cam.projection.value.fromArray(projection); + cam.projectionview.value.fromArray(projectionview); + cam.pixel_space.value.fromArray(pixel_space); + cam.resolution.value.fromArray(resolution_scaled); + cam.eyeposition.value.fromArray(JSServe.deserialize_js(eyepos)); + } + + update_cam(JSServe.get_observable(data.camera)); + + if (data.cam3d_state) { + attach_3d_camera(canvas, cam, data.cam3d_state); + } else { + JSServe.on_update(data.camera, update_cam); + } + + data.plots.forEach((plot_data) => { + add_plot(scene, plot_data); + }); + return scene; + } + + function connect_uniforms(mesh, updater) { + JSServe.on_update(updater, ([name, data]) => { + // this is the initial value, which shouldn't end up getting updated - + // TODO, figure out why this gets pushed!! + if (name === "none"){ + return + } + const uniform = mesh.material.uniforms[name]; + const deserialized = deserialize_three(JSServe.deserialize_js(data)); + + if (uniform.value.isTexture) { + uniform.value.image.data.set(deserialized); + uniform.value.needsUpdate = true; + } else { + uniform.value = deserialized; + } + }); + } + + function first(x) { + return x[Object.keys(x)[0]]; + } + + function connect_attributes(mesh, updater) { + const instance_buffers = {}; + const geometry_buffers = {}; + let first_instance_buffer; + const real_instance_length = [0]; + let first_geometry_buffer; + const real_geometry_length = [0]; + + function re_assign_buffers() { + const attributes = mesh.geometry.attributes; + const buffers = Object.values(attributes); + Object.keys(attributes).forEach((name) => { + const buffer = attributes[name]; + if (buffer.isInstancedBufferAttribute) { + instance_buffers[name] = buffer; + } else { + geometry_buffers[name] = buffer; + } + }); + first_instance_buffer = first(instance_buffers); + // not all meshes have instances! + if (first_instance_buffer) { + real_instance_length[0] = first_instance_buffer.count; + } + first_geometry_buffer = first(geometry_buffers); + real_geometry_length[0] = first_geometry_buffer.count; + } + + re_assign_buffers(); + + JSServe.on_update(updater, ([name, array, length]) => { + // TODO, why are these called with the initial values!? + if (length > 0) { + const new_values = deserialize_three(JSServe.deserialize_js(array)); + const buffer = mesh.geometry.attributes[name]; + let buffers; + let first_buffer; + let real_length; + let is_instance = false; + // First, we need to figure out if this is an instance / geometry buffer + if (name in instance_buffers) { + buffers = instance_buffers; + first_buffer = first_instance_buffer; + real_length = real_instance_length; + is_instance = true; + } else { + buffers = geometry_buffers; + first_buffer = first_geometry_buffer; + real_length = real_geometry_length; + } + if (length <= real_length[0]) { + // this is simple - we can just update the values + buffer.set(new_values); + buffer.needsUpdate = true; + if (is_instance) { + mesh.geometry.instanceCount = length; + } + } else { + // resizing is a bit more complex + // first we directly overwrite the array - this + // won't have any effect, but like this we can collect the + // newly sized arrays untill all of them have the same length + buffer.to_update = new_values; + if ( + Object.values(buffers).every( + (x) => + x.to_update && + x.to_update.length / x.itemSize == length + ) + ) { + if (is_instance) { + recreate_instanced_geometry(mesh); + // we just replaced geometry & all buffers, so we need to update thise + re_assign_buffers(); + mesh.geometry.instanceCount = + new_values.length / buffer.itemSize; + } + } + } + } + }); + } + + function render_scene(renderer, scene, cam) { + renderer.autoClear = scene.clearscene; + const area = JSServe.get_observable(scene.pixelarea); + if (area) { + const [x, y, w, h] = area.map(t => t / pixelRatio); + renderer.setViewport(x, y, w, h); + renderer.setScissor(x, y, w, h); + renderer.setScissorTest(true); + renderer.setClearColor( + JSServe.get_observable(scene.backgroundcolor) + ); + renderer.render(scene, cam); + } + } + + function render_scenes(renderer, scenes, cam) { + scenes.forEach((scene) => render_scene(renderer, scene, cam)); + } + + function start_renderloop(renderer, three_scenes, cam) { + function renderloop() { + const canvas = renderer.domElement + if (!document.body.contains(canvas)){ + console.log("EXITING WGL") + renderer.state.reset() + renderer.dispose() + return + } + render_scenes(renderer, three_scenes, cam); + window.requestAnimationFrame(renderloop); + } + renderloop(); + } + + function threejs_module(canvas, comm, width, height) { + 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.setClearColor("#ffffff"); + + // The following handles high-DPI devices + // `renderer.setSize` also updates `canvas` size + renderer.setPixelRatio(pixelRatio); + renderer.setSize(width / pixelRatio, height / pixelRatio); + + function mousemove(event) { + var rect = canvas.getBoundingClientRect(); + var x = (event.clientX - rect.left) * pixelRatio; + var y = (event.clientY - rect.top) * pixelRatio; + JSServe.update_obs(comm, { + mouseposition: [x, y], + }); + return false; + } + + canvas.addEventListener("mousemove", mousemove); + + function mousedown(event) { + JSServe.update_obs(comm, { + mousedown: event.buttons, + }); + return false; + } + canvas.addEventListener("mousedown", mousedown); + + function mouseup(event) { + JSServe.update_obs(comm, { + mouseup: event.buttons, + }); + return false; + } + + canvas.addEventListener("mouseup", mouseup); + + function wheel(event) { + JSServe.update_obs(comm, { + scroll: [event.deltaX, -event.deltaY], + }); + event.preventDefault(); + return false; + } + canvas.addEventListener("wheel", wheel); + + function keydown(event) { + JSServe.update_obs(comm, { + keydown: event.code, + }); + return false; + } + + canvas.addEventListener("keydown", keydown); + + function keyup(event) { + JSServe.update_obs(comm, { + keyup: event.code, + }); + return false; + } + + canvas.addEventListener("keyup", keyup); + // This is a pretty ugly work around...... + // so on keydown, we add the key to the currently pressed keys set + // if we open the contextmenu before releasing the key, we'll never + // receive an up event, so the key will stay inside the currently_pressed + // set... Only option I found is to actually listen to the contextmenu + // and remove all keys if its opened. + function contextmenu(event) { + JSServe.update_obs(comm, { + keyup: "delete_keys", + }); + return false; + } + + canvas.addEventListener("contextmenu", e => e.preventDefault()) + canvas.addEventListener("focusout", contextmenu); + + return renderer; + } + + return { + deserialize_scene, + threejs_module, + start_renderloop, + deserialize_three, + render_scenes, + delete_plots, + insert_plot, + find_plots, + delete_scene, + find_scene, + scene_cache, + plot_cache, + delete_scenes, + }; +})(); diff --git a/WGLMakie/test/Project.toml b/WGLMakie/test/Project.toml new file mode 100644 index 00000000000..051787402a5 --- /dev/null +++ b/WGLMakie/test/Project.toml @@ -0,0 +1,13 @@ +[deps] +Electron = "a1bb12fb-d4d1-54b4-b10a-ee7951ef7ad3" +ElectronDisplay = "d872a56f-244b-5cc9-b574-2017b5b909a8" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +JSServe = "824d6782-a2ef-11e9-3a09-e5662e0c26f9" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +ReferenceTests = "d37af2e0-5618-4e00-9939-d430db56ee94" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +Electron = "3.1.1" diff --git a/WGLMakie/test/offline_export.jl b/WGLMakie/test/offline_export.jl new file mode 100644 index 00000000000..0d08ca7c066 --- /dev/null +++ b/WGLMakie/test/offline_export.jl @@ -0,0 +1,13 @@ +using JSServe, WGLMakie, Makie + +function handler(session, request) + return scatter(1:4, color=1:4) +end + +dir = joinpath(@__DIR__, "exported") +isdir(dir) || mkdir(dir) +JSServe.export_standalone(handler, dir) +# Then serve it with e.g. LiveServer +using LiveServer + +LiveServer.serve(dir=dir) diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl new file mode 100644 index 00000000000..6557a484d03 --- /dev/null +++ b/WGLMakie/test/runtests.jl @@ -0,0 +1,41 @@ +using ElectronDisplay +ElectronDisplay.CONFIG.showable = showable +ElectronDisplay.CONFIG.single_window = true +ElectronDisplay.CONFIG.focus = false +using ImageMagick, FileIO +using WGLMakie, Makie, Test +using ReferenceTests +using ReferenceTests: database_filtered + +excludes = Set([ + "Streamplot animation", + "Transforming lines", + "image scatter", + "Line GIF", + "surface + contour3d", + # Hm weird, looks like some internal JSServe error missing an Observable: + "Errorbars x y low high", + "Rangebars x y low high", + # These are a bit sad, since it's just missing interpolations + "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", + "Stars", + "heatmaps & surface", + "OldAxis + Surface" +]) +excludes2 = Set(["short_tests_83", "short_tests_78", "short_tests_40", "short_tests_13", "short_tests_5", "short_tests_41"]) +database = database_filtered(excludes, excludes2) + +recorded = joinpath(@__DIR__, "recorded") +rm(recorded; force=true, recursive=true); mkdir(recorded) +ReferenceTests.record_tests(database; recording_dir=recorded) +ReferenceTests.reference_tests(recorded; difference=0.06) diff --git a/docs/src/assets/NotoSans-Bold.ttf b/docs/src/assets/NotoSans-Bold.ttf deleted file mode 100755 index 54ad879b41b..00000000000 Binary files a/docs/src/assets/NotoSans-Bold.ttf and /dev/null differ diff --git a/docs/src/assets/NotoSans-Regular.ttf b/docs/src/assets/NotoSans-Regular.ttf deleted file mode 100644 index 04be6f5eee1..00000000000 Binary files a/docs/src/assets/NotoSans-Regular.ttf and /dev/null differ diff --git a/docs/src/assets/cow.png b/docs/src/assets/cow.png deleted file mode 100644 index 4e6787b5604..00000000000 Binary files a/docs/src/assets/cow.png and /dev/null differ diff --git a/docs/src/backends_and_output.md b/docs/src/backends_and_output.md index 36b2ca5ce09..841018d6ee2 100644 --- a/docs/src/backends_and_output.md +++ b/docs/src/backends_and_output.md @@ -7,9 +7,9 @@ There are three main backends which concretely implement all abstract rendering | Package | Description | | :------------------------------------------------------------- | :------------------------------------------------------------------------------------ | -| [`GLMakie.jl`](https://github.com/JuliaPlots/GLMakie.jl) | GPU-powered, interactive 2D and 3D plotting in standalone `GLFW.jl` windows. | -| [`CairoMakie.jl`](https://github.com/JuliaPlots/CairoMakie.jl) | `Cairo.jl` based, non-interactive 2D backend for publication-quality vector graphics. | -| [`WGLMakie.jl`](https://github.com/JuliaPlots/WGLMakie.jl) | WebGL-based interactive 2D and 3D plotting that runs within browsers. | +| [`GLMakie.jl`](https://github.com/JuliaPlots/Makie.jl/tree/master/GLMakie) | GPU-powered, interactive 2D and 3D plotting in standalone `GLFW.jl` windows. | +| [`CairoMakie.jl`](https://github.com/JuliaPlots/Makie.jl/tree/master/CairoMakie) | `Cairo.jl` based, non-interactive 2D backend for publication-quality vector graphics. | +| [`WGLMakie.jl`](https://github.com/JuliaPlots/Makie.jl/tree/master/WGLMakie) | WebGL-based interactive 2D and 3D plotting that runs within browsers. | ### Activating Backends @@ -22,7 +22,7 @@ using WGLMakie WGLMakie.activate!() ``` -## [GLMakie](https://github.com/JuliaPlots/GLMakie.jl) +## [GLMakie](https://github.com/JuliaPlots/Makie.jl/tree/master/GLMakie) GLMakie is the native, desktop-based backend, and is the most feature-complete. It requires an OpenGL enabled graphics card with OpenGL version 3.3 or higher. @@ -46,7 +46,7 @@ set_window_config!(; ) ``` -## [CairoMakie](https://github.com/JuliaPlots/CairoMakie.jl) +## [CairoMakie](https://github.com/JuliaPlots/Makie.jl/tree/master/CairoMakie) CairoMakie uses Cairo.jl to draw vector graphics to SVG and PDF. You should use it if you want to achieve the highest-quality plots for publications, as the rendering process of the GL backends works via bitmaps and is geared more towards speed than pixel-perfection. @@ -94,7 +94,7 @@ The z-values of 3D plots will have no effect and will be projected flat onto the Z-layering is approximated by sorting all plot objects by their z translation value before drawing, after that by parent scene and then insertion order. Therefore, if you want to draw something on top of something else, but it ends up below, try translating it forward via `translate!(obj, 0, 0, some_positive_z_value)`. -## [WGLMakie](https://github.com/JuliaPlots/WGLMakie.jl) +## [WGLMakie](https://github.com/JuliaPlots/Makie.jl/tree/master/WGLMakie) WGLMakie is the Web-based backend, and is still experimental (though relatively feature-complete). Only serving it on a webpage or in Pluto.jl / Ijulia are currently supported. VSCode integration should come soon. diff --git a/docs/src/index.md b/docs/src/index.md index 35ea0e99ab7..aa701062c37 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -6,7 +6,7 @@ Makie is a high-performance, extendable, and multi-platform plotting ecosystem f ## Installation and Import -Add one or more of the Makie backend packages [`GLMakie.jl`](https://github.com/JuliaPlots/GLMakie.jl) (OpenGL), [`CairoMakie.jl`](https://github.com/JuliaPlots/CairoMakie.jl) (Cairo), or [`WGLMakie.jl`](https://github.com/JuliaPlots/WGLMakie.jl) (WebGL). +Add one or more of the Makie backend packages [`GLMakie.jl`](https://github.com/JuliaPlots/Makie.jl/tree/master/GLMakie) (OpenGL), [`CairoMakie.jl`](https://github.com/JuliaPlots/Makie.jl/tree/master/CairoMakie) (Cairo), or [`WGLMakie.jl`](https://github.com/JuliaPlots/Makie.jl/tree/master/WGLMakie) (WebGL). ```julia ]add GLMakie @@ -54,9 +54,9 @@ Markdown.parse(""" | Package | Description | | :------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | | [`Makie.jl`](https://github.com/JuliaPlots/Makie.jl) | Defines all infrastructure objects which can be visualized using one of the three backend packages. | -| [`GLMakie.jl`](https://github.com/JuliaPlots/GLMakie.jl) | GPU-powered, interactive 2D and 3D plotting in standalone `GLFW.jl` windows. | -| [`CairoMakie.jl`](https://github.com/JuliaPlots/CairoMakie.jl) | `Cairo.jl` based, non-interactive 2D backend for publication-quality vector graphics. | -| [`WGLMakie.jl`](https://github.com/JuliaPlots/WGLMakie.jl) | WebGL-based interactive 2D and 3D plotting that runs within browsers. | +| [`GLMakie.jl`](https://github.com/JuliaPlots/Makie.jl/tree/master/GLMakie) | GPU-powered, interactive 2D and 3D plotting in standalone `GLFW.jl` windows. | +| [`CairoMakie.jl`](https://github.com/JuliaPlots/Makie.jl/tree/master/CairoMakie) | `Cairo.jl` based, non-interactive 2D backend for publication-quality vector graphics. | +| [`WGLMakie.jl`](https://github.com/JuliaPlots/Makie.jl/tree/master/WGLMakie) | WebGL-based interactive 2D and 3D plotting that runs within browsers. | The differences between backends are explained in more details under [Backends & Output](@ref). diff --git a/docs/src/makielayout/button.md b/docs/src/makielayout/button.md index 4efe69b795d..e6e381ccfde 100644 --- a/docs/src/makielayout/button.md +++ b/docs/src/makielayout/button.md @@ -7,7 +7,7 @@ CairoMakie.activate!() ```@example using GLMakie - +GLMakie.activate!() # hide fig = Figure() ax = Axis(fig[1, 1]) diff --git a/docs/src/makielayout/intervalslider.md b/docs/src/makielayout/intervalslider.md index 3d431b89bfb..d9fb44f74ca 100644 --- a/docs/src/makielayout/intervalslider.md +++ b/docs/src/makielayout/intervalslider.md @@ -19,8 +19,7 @@ If `startvalues === Makie.automatic`, the full interval will be selected (this i If you set the attribute `snap = false`, the slider will move continously while dragging and only jump to the closest available values when releasing the mouse. ```@example -using GLMakie -using CairoMakie # hide +using CairoMakie Makie.inline!(true) # hide CairoMakie.activate!() # hide diff --git a/docs/src/makielayout/menu.md b/docs/src/makielayout/menu.md index 6881a1709a5..c5e81a95565 100644 --- a/docs/src/makielayout/menu.md +++ b/docs/src/makielayout/menu.md @@ -15,7 +15,7 @@ The attribute `selection` is set to `optionvalue(element)` when the element's en ```@example using GLMakie - +GLMakie.activate!() # hide fig = Figure() menu = Menu(fig, options = ["viridis", "heat", "blues"]) diff --git a/docs/src/makielayout/slider.md b/docs/src/makielayout/slider.md index 56c42f26c28..09cbee4b2d5 100644 --- a/docs/src/makielayout/slider.md +++ b/docs/src/makielayout/slider.md @@ -18,7 +18,7 @@ If you set the attribute `snap = false`, the slider will move continously while ```@example using GLMakie - +GLMakie.activate!() # hide fig = Figure() ax = Axis(fig[1, 1]) diff --git a/docs/src/makielayout/toggle.md b/docs/src/makielayout/toggle.md index d497e7d6c5d..2b7c526e832 100644 --- a/docs/src/makielayout/toggle.md +++ b/docs/src/makielayout/toggle.md @@ -10,7 +10,7 @@ or disable properties of an interactive plot. ```@example using GLMakie - +GLMakie.activate!() # hide fig = Figure() ax = Axis(fig[1, 1]) diff --git a/docs/src/plot_method_signatures.md b/docs/src/plot_method_signatures.md index 0feef5983e3..69070aef953 100644 --- a/docs/src/plot_method_signatures.md +++ b/docs/src/plot_method_signatures.md @@ -35,7 +35,7 @@ Here are two examples with the scatter function (take care to create single-argu ```@example using GLMakie - +GLMakie.activate!() # hide # FigureAxisPlot takes figure and axis keywords fig, ax, p = lines(cumsum(randn(1000)), figure = (resolution = (1000, 600),), @@ -77,7 +77,7 @@ If a GridLayout along the nesting levels doesn't exist, yet, it is created autom ```@example using GLMakie - +GLMakie.activate!() # hide fig = Figure() # first row, first column @@ -110,7 +110,7 @@ If it's not the case, the function will error. ```@example using GLMakie - +GLMakie.activate!() # hide fig = Figure() lines(fig[1, 1], 1.0..10, sin, color = :blue) diff --git a/src/Makie.jl b/src/Makie.jl index ad92f169d72..6d5b4fa0f82 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -1,5 +1,12 @@ module Makie +module ContoursHygiene + import Contour +end + +using .ContoursHygiene +const Contours = ContoursHygiene.Contour + using Artifacts using Random using FFMPEG # get FFMPEG on any system! @@ -22,27 +29,30 @@ using Printf: @sprintf import Isoband import PolygonOps import GridLayoutBase +using MakieCore + +import MakieCore: plot, plot!, theme, plotfunc, plottype, merge_attributes!, calculated_attributes!, get_attribute, plotsym, plotkey, attributes, used_attributes + +using MakieCore: SceneLike, AbstractScreen, ScenePlot, AbstractScene, AbstractPlot, Transformable, Attributes, Combined, Theme, Plot + +using MakieCore: Heatmap, Image, Lines, LineSegments, Mesh, MeshScatter, Scatter, Surface, Text, Volume +import MakieCore: heatmap, image, lines, linesegments, mesh, meshscatter, scatter, surface, text, volume +import MakieCore: heatmap!, image!, lines!, linesegments!, mesh!, meshscatter!, scatter!, surface!, text!, volume! + +import MakieCore: convert_arguments, convert_attribute, default_theme, conversion_trait +using MakieCore: ConversionTrait, NoConversion, PointBased, SurfaceLike, ContinuousSurface, DiscreteSurface, VolumeLike +export ConversionTrait, NoConversion, PointBased, SurfaceLike, ContinuousSurface, DiscreteSurface, VolumeLike +using MakieCore: Key, @key_str, Automatic, automatic, @recipe +using MakieCore: Pixel, px, Unit, Billboard +export Pixel, px, Unit, plotkey, attributes, used_attributes + using StatsFuns: logit, logistic # Imports from Base which we don't want to have to qualify using Base: RefValue using Base.Iterators: repeated, drop import Base: getindex, setindex!, push!, append!, parent, get, get!, delete!, haskey -using Observables: listeners, to_value - -# Backwards compatability for Observables 0.3 -if hasmethod(Observables.notify, Tuple{Observable}) - using Observables: notify -else - Base.notify(obs::Observable) = Observables.notify!(obs) -end - -module ContoursHygiene - import Contour -end - -using .ContoursHygiene -const Contours = ContoursHygiene.Contour +using Observables: listeners, to_value, notify const RealVector{T} = AbstractVector{T} where T <: Number const Node = Observable # shorthand @@ -53,8 +63,6 @@ const NativeFont = FreeTypeAbstraction.FTFont include("documentation/docstringextension.jl") include("utilities/quaternions.jl") -include("attributes.jl") -include("dictlike.jl") include("interaction/PriorityObservable.jl") include("types.jl") include("utilities/utilities.jl") @@ -96,7 +104,6 @@ include("themes/theme_black.jl") include("themes/theme_minimal.jl") include("themes/theme_light.jl") include("themes/theme_dark.jl") -include("recipes.jl") include("interfaces.jl") include("units.jl") include("conversions.jl") @@ -288,9 +295,8 @@ end include("figureplotting.jl") include("basic_recipes/series.jl") -if Base.VERSION >= v"1.4.2" - include("precompile.jl") - _precompile_() -end +export Heatmap, Image, Lines, LineSegments, Mesh, MeshScatter, Scatter, Surface, Text, Volume +export heatmap, image, lines, linesegments, mesh, meshscatter, scatter, surface, text, volume +export heatmap!, image!, lines!, linesegments!, mesh!, meshscatter!, scatter!, surface!, text!, volume! end # module diff --git a/src/attributes.jl b/src/attributes.jl deleted file mode 100644 index 39eec8ad481..00000000000 --- a/src/attributes.jl +++ /dev/null @@ -1,279 +0,0 @@ - -""" -Main structure for holding attributes in e.g. plots! -""" -struct ObservableAttributes - # name, for better error messages! - name::String - # We dont have one node per value anymore, but instead one node - # that gets triggered on any setindex!, or whenever an input attribute node changes - # This makes it easier to layer Observable{ObservableAttributes}() - on_change::Observable{Pair{Symbol, Any}} - # The supported fields, so we can throw an error, whenever fields are not supported - supported_fields::Set{Symbol} - # The attributes given at construction time, taking the highest priority - from_user::Dict{Symbol, Any} - # attributes filled in by e.g. a theme, or other processes in the pipeline - from_theme::Dict{Symbol, Any} - # The "global" default values for this specific attribute instance - # Will be immutabe (maybe not a Dict then?) and shared between all similar objects - default_values::Dict{Symbol, Any} - - function ObservableAttributes(name::String, default_values::Dict{Symbol, Any}, from_user::Dict{Symbol, Any}) - on_change = Observable{Pair{Symbol, Any}}() - supported_fields = Set(keys(default_values)) - return new(name, on_change, supported_fields, from_user, - Dict{Symbol, Any}(), default_values) - end -end - -function ObservableAttributes(;kw...) - return ObservableAttributes(kw) -end - -value_convert2(x::Observables.AbstractObservable) = x[] -value_convert2(@nospecialize(x)) = x -function value_convert2(x::NTuple{N, Union{Any, Observables.AbstractObservable}}) where N - return to_value.(x) -end -value_convert2(x::NamedTuple) = ObservableAttributes(x) - -function ObservableAttributes(kw) - defaults = Dict{Symbol, Any}() - for (k, v) in pairs(kw) - xx = value_convert2(v) - defaults[k] = xx - @assert !(xx isa Observable) - end - attributes = ObservableAttributes("", defaults, Dict{Symbol, Any}()) - for (k, v) in pairs(kw) - if v isa Observables.AbstractObservable - on(v) do value - setproperty!(attributes, k, value) - end - end - end - return attributes -end - -function Base.getindex(attributes::ObservableAttributes, field::Symbol) - return getproperty(attributes, field) -end - -function Base.getindex(attributes::ObservableAttributes, field::Symbol, b::Symbol) - x = getproperty(attributes, b) - return getproperty(x, field) -end - -function Base.propertynames(attributes::ObservableAttributes) - return getfield(attributes, :supported_fields) -end - -function on_change(x::ObservableAttributes) - return getfield(x, :on_change) -end - -function on_change(f, x::ObservableAttributes) - return on(f, on_change) -end - -""" -attributes.attribute returns an observable. -Since we don't actually convert all values to Observables anymore, -we'll need to create new Observables on getproperty. -With OnFieldUpdate, we can do that lazily, store them in `listeners(onchange(obs))`, -and only create a new one for fields that aren't in listeners yet. -""" -struct OnFieldUpdate - field::Symbol - observable::Observable -end - -function (of::OnFieldUpdate)(field_value::Pair{Symbol, <: Any}) - if field_value[1] === of.field - of.observable[] = field_value[2] - end - return -end - -function OnFieldUpdate(attributes::ObservableAttributes, field::Symbol) - # We lazily store observables on field updates in the listeners - # If we already have it in there, we just return the one we have. - onchange = on_change(attributes) - for listener in Observables.listeners(onchange) - if listener isa OnFieldUpdate && listener.field === field - return listener - end - end - # we haven't found a listener, so we create a new one! - result = Observable{Any}(get_value(attributes, field)) - of = OnFieldUpdate(field, result) - on(of, onchange) - return of -end - -""" - get_value(attributes::ObservableAttributes, field::Symbol) -Gets the value for `field`. -The values are looked up in the following order: - 1) user given at creation time - 2) theme given - 3) global defaults -""" -function get_value(attributes::ObservableAttributes, field::Symbol) - name = getfield(attributes, :name) - if field in propertynames(attributes) - # The priority is: - # User given - from_user = getfield(attributes, :from_user) - haskey(from_user, field) && return from_user[field] - # ObservableAttributes given - from_theme = getfield(attributes, :from_theme) - haskey(from_theme, field) && return from_theme[field] - # Construction defaults - default_values = getfield(attributes, :default_values) - haskey(default_values, field) && return default_values[field] - error("Incorrectly constructed ObservableAttributes ($(name))! No value found for $(field)") - else - from_user = getfield(attributes, :from_user) - default_values = getfield(attributes, :default_values) - pn = propertynames(attributes) - error("Field $(field) not in $(pn) $(from_user) $(default_values)!") - end -end - -function Base.getproperty(attributes::ObservableAttributes, field::Symbol) - val = get_value(attributes, field) - val isa ObservableAttributes && return val - of = OnFieldUpdate(attributes, field) - return of.observable -end - -function Base.setproperty!(attributes::ObservableAttributes, field::Symbol, value) - name = getfield(attributes, :name) - # we always set the users data, since setting this is done by the user! - from_user = getfield(attributes, :from_user) - from_user[field] = value - on_change = getfield(attributes, :on_change) - # trigger change! - on_change[] = field => value - if !(field in propertynames(attributes)) - # TODO, dont let anyone change propertynames! - push!(propertynames(attributes), field) - default_values = getfield(attributes, :default_values) - default_values[field] = value - end - return value -end - -function Base.setproperty!(attributes::ObservableAttributes, field::Symbol, value::Observable) - setproperty!(attributes, field, value[]) - on(value) do new_value - setproperty!(attributes, field, value[]) - end - return value -end - -function Base.setindex!(attributes::ObservableAttributes, value, field::Symbol) - setproperty!(attributes, field, value) -end - -Base.broadcastable(x::ObservableAttributes) = Ref(x) - -####### -## Dict interface -Base.keys(x::ObservableAttributes) = propertynames(x) -Base.values(x::ObservableAttributes) = (getproperty(x, field) for field in propertynames(x)) -function Base.pop!(x::ObservableAttributes, field::Symbol) - value = getproperty(x, field) - delete!(x, field) - return value -end - -Base.haskey(x::ObservableAttributes, key::Symbol) = key in keys(x) - -Base.filter(f, x::ObservableAttributes) = ObservableAttributes(filter(f, attributes(x))) -Base.empty!(x::ObservableAttributes) = (empty!(attributes(x)); x) -Base.length(x::ObservableAttributes) = length(attributes(x)) - -function Base.delete!(attributes::ObservableAttributes, field::Symbol) - # The priority is: - # User given - from_user = getfield(attributes, :from_user) - haskey(from_user, field) && delete!(from_user, field) - # ObservableAttributes given - from_theme = getfield(attributes, :from_theme) - haskey(from_theme, field) && delete!(from_theme, field) - # Construction defaults - default_values = getfield(attributes, :default_values) - haskey(default_values, field) && delete!(default_values, field) - delete!(propertynames(attributes), field) - return -end - -function Base.iterate(x::ObservableAttributes, state...) - s = iterate(keys(x), state...) - s === nothing && return nothing - return (s[1] => x[s[1]], s[2]) -end - -function Base.copy(attributes::ObservableAttributes) - return ObservableAttributes(attributes) -end - -Base.merge(target::ObservableAttributes, args::ObservableAttributes...) = merge!(copy(target), args...) - -function merge_attributes!(input::ObservableAttributes, theme::ObservableAttributes) - for (key, value) in theme - if !haskey(input, key) - input[key] = value - else - current_value = input[key] - if value isa ObservableAttributes && current_value isa ObservableAttributes - # if nested attribute, we merge recursively - merge_attributes!(current_value, value) - elseif value isa ObservableAttributes || current_value isa ObservableAttributes - error(""" - Type missmatch while merging plot attributes with theme for key: $(key). - Found $(to_value(value)) in theme, while attributes contains: $(current_value) - """) - else - # we're good! input already has a value, can ignore theme - end - end - end - return input -end - -function Base.merge!(target::ObservableAttributes, args::ObservableAttributes...) - for elem in args - merge_attributes!(target, elem) - end - return target -end - -function Base.show(io::IO,::MIME"text/plain", attr::ObservableAttributes) - d = Dict() - for (k, v) in attr - d[k] = to_value(v) - end - show(IOContext(io, :limit => false), MIME"text/plain"(), d) -end - -Base.show(io::IO, attr::ObservableAttributes) = show(io, MIME"text/plain"(), attr) - -""" - get_attribute(dict::ObservableAttributes, key::Key) -Gets the attribute at `key`, converts it and extracts the value -""" -function get_attribute(dict::ObservableAttributes, key::Symbol) - return convert_attribute(get_value(dict, key), Key{key}()) -end - -""" - get_attribute(dict::ObservableAttributes, key::Key) -Gets the attribute at `key` as a converted signal -""" -function get_lifted_attribute(dict::ObservableAttributes, key::Symbol) - return lift(x-> convert_attribute(x, Key{key}()), getproperty(dict, key)) -end diff --git a/src/conversions.jl b/src/conversions.jl index 6fe70e450f1..d1660823750 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -1,6 +1,7 @@ ################################################################################ # Type Conversions # ################################################################################ +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...) @@ -59,35 +60,6 @@ function recursively_convert_argument(x) end end -################################################################################ -# Conversion Traits # -################################################################################ - -abstract type ConversionTrait end - -const XYBased = Union{MeshScatter, Scatter, Lines, LineSegments} -const RangeLike = Union{AbstractRange, AbstractVector, ClosedInterval} - -struct NoConversion <: ConversionTrait end - -# No conversion by default -conversion_trait(::Type) = NoConversion() -convert_arguments(::NoConversion, args...) = args - -struct PointBased <: ConversionTrait end -conversion_trait(x::Type{<: XYBased}) = PointBased() - -abstract type SurfaceLike <: ConversionTrait end - -struct ContinuousSurface <: SurfaceLike end -conversion_trait(::Type{<: Union{Surface, Image}}) = ContinuousSurface() - -struct DiscreteSurface <: SurfaceLike end -conversion_trait(::Type{<: Heatmap}) = DiscreteSurface() - -struct VolumeLike end -conversion_trait(::Type{<: Volume}) = VolumeLike() - ################################################################################ # Single Argument Conversion # ################################################################################ @@ -757,10 +729,14 @@ convert_attribute(b::Billboard{Float32}, ::key"rotations") = to_rotation(b.rotat convert_attribute(b::Billboard{Vector{Float32}}, ::key"rotations") = to_rotation.(b.rotation) convert_attribute(r::AbstractArray, ::key"rotations") = to_rotation.(r) convert_attribute(r::StaticVector, ::key"rotations") = to_rotation(r) +convert_attribute(r, ::key"rotations") = to_rotation(r) convert_attribute(c, ::key"markersize", ::key"scatter") = to_2d_scale(c) convert_attribute(c, k1::key"markersize", k2::key"meshscatter") = to_3d_scale(c) +convert_attribute(x, ::key"uv_offset_width") = Vec4f0(x) +convert_attribute(x::AbstractVector{Vec4f0}, ::key"uv_offset_width") = x + to_2d_scale(x::Number) = Vec2f0(x) to_2d_scale(x::VecTypes) = to_ndim(Vec2f0, x, 1) to_2d_scale(x::Tuple{<:Number, <:Number}) = to_ndim(Vec2f0, x, 1) @@ -895,6 +871,7 @@ convert_attribute(c::VecTypes{N}, ::key"position") where N = Point{N, Float32}(c """ convert_attribute(x::Tuple{Symbol, Symbol}, ::key"align") = Vec2f0(alignment2num.(x)) convert_attribute(x::Vec2f0, ::key"align") = x + const _font_cache = Dict{String, NativeFont}() """ @@ -933,15 +910,14 @@ end convert_attribute(x::Vector{String}, k::key"font") = convert_attribute.(x, k) convert_attribute(x::NativeFont, k::key"font") = x - - """ rotation accepts: to_rotation(b, quaternion) to_rotation(b, tuple_float) to_rotation(b, vec4) """ -convert_attribute(s::Quaternion, ::key"rotation") = s +convert_attribute(s::Quaternionf0, ::key"rotation") = s +convert_attribute(s::Quaternion, ::key"rotation") = Quaternionf0(s.data...) function convert_attribute(s::VecTypes{N}, ::key"rotation") where N if N == 4 Quaternionf0(s...) @@ -963,7 +939,6 @@ convert_attribute(r::AbstractVector, k::key"rotation") = to_rotation.(r) convert_attribute(r::AbstractVector{<: Quaternionf0}, k::key"rotation") = r - convert_attribute(x, k::key"colorrange") = x==nothing ? nothing : Vec2f0(x) convert_attribute(x, k::key"textsize") = Float32(x) @@ -1217,3 +1192,15 @@ end convert_attribute(value, ::key"marker", ::key"scatter") = to_spritemarker(value) convert_attribute(value, ::key"isovalue", ::key"volume") = Float32(value) convert_attribute(value, ::key"isorange", ::key"volume") = Float32(value) + +function convert_attribute(value::Symbol, ::key"marker", ::key"meshscatter") + if value == :Sphere + return normal_mesh(Sphere(Point3f0(0), 1f0)) + else + error("Unsupported marker: $(value)") + end +end + +function convert_attribute(value::AbstractGeometry, ::key"marker", ::key"meshscatter") + return normal_mesh(value) +end diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index a1f93617d44..6f86ed21829 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -42,7 +42,7 @@ function color2text(c::RGBAf0) @sprintf("RGB(%0.2f, %0.2f, %0.2f)", c.r, c.g, c.b) else @sprintf("RGBA(%0.2f, %0.2f, %0.2f, %0.2f)", c.r, c.g, c.b, c.alpha) - end + end end color2text(name, i::Integer, j::Integer, c) = "$name[$i, $j] = $(color2text(c))" @@ -224,8 +224,8 @@ end # Text text_padding = Vec4f0(5, 5, 3, 3), # LRBT text_align = (:left, :bottom), - textcolor = :black, - textsize = 18, + textcolor = :black, + textsize = 18, font = theme(scene, :font), _display_text = " ", _text_position = Point2f0(0), @@ -242,7 +242,7 @@ end indicator_linestyle = nothing, _bbox2D = FRect2D(Vec2f0(0), Vec2f0(0)), _px_bbox_visible = true, - + # general tooltip_align = (:center, :top), # default tooltip position relative to cursor tooltip_offset = Vec2f0(20), # default offset in alignment direction @@ -266,19 +266,19 @@ function plot!(plot::_Inspector) text_padding, text_align, textcolor, textsize, font, background_color, outline_color, outline_linestyle, outline_linewidth, indicator_linestyle, indicator_linewidth, indicator_color, - tooltip_offset, depth, - _display_text, _text_position, _bbox2D, _px_bbox_visible, + tooltip_offset, depth, + _display_text, _text_position, _bbox2D, _px_bbox_visible, _tooltip_align, _root_px_projection, _visible ) # tooltip text _aligned_text_position = Node(Point2f0(0)) id = Mat4f0(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1) - text_plot = text!(plot, _display_text, + text_plot = text!(plot, _display_text, position = _aligned_text_position, visible = _visible, align = text_align, color = textcolor, font = font, textsize = textsize, show_axis = false, - inspectable = false, - # with https://github.com/JuliaPlots/GLMakie.jl/pull/183 this should + inspectable = false, + # with https://github.com/JuliaPlots/Makie.jl/tree/master/GLMakie/pull/183 this should # allow the tooltip to work in any scene. space = :data, projection = _root_px_projection, view = id, projectionview = _root_px_projection @@ -289,7 +289,7 @@ function plot!(plot::_Inspector) rect = Bbox_from_glyphlayout(_display_text[], gl) l, r, b, t = pad FRect2D( - origin(rect) .+ Vec2f0(pos[1] - l, pos[2] - b), + origin(rect) .+ Vec2f0(pos[1] - l, pos[2] - b), widths(rect) .+ Vec2f0(l + r, b + t) ) end @@ -318,9 +318,9 @@ function plot!(plot::_Inspector) end - # tooltip background and frame + # tooltip background and frame background = mesh!( - plot, bbox, color = background_color, shading = false, #fxaa = false, + plot, bbox, color = background_color, shading = false, #fxaa = false, # TODO with fxaa here the text above becomes seethrough on a heatmap visible = _visible, show_axis = false, inspectable = false, projection = _root_px_projection, view = id, projectionview = _root_px_projection @@ -328,15 +328,15 @@ function plot!(plot::_Inspector) outline = wireframe!( plot, bbox, color = outline_color, visible = _visible, show_axis = false, inspectable = false, - linestyle = outline_linestyle, linewidth = outline_linewidth, + linestyle = outline_linestyle, linewidth = outline_linewidth, projection = _root_px_projection, view = id, projectionview = _root_px_projection ) - + # pixel-space marker for selected element (not always used) px_bbox = wireframe!( plot, _bbox2D, - color = indicator_color, linewidth = indicator_linewidth, - linestyle = indicator_linestyle, visible = _px_bbox_visible, + color = indicator_color, linewidth = indicator_linewidth, + linestyle = indicator_linestyle, visible = _px_bbox_visible, show_axis = false, inspectable = false, projection = _root_px_projection, view = id, projectionview = _root_px_projection ) @@ -393,17 +393,17 @@ disable!(inspector::DataInspector) = inspector.plot.enabled[] = false """ DataInspector(figure; kwargs...) -Creates a data inspector which will show relevant information in a tooltip -when you hover over a plot. If you wish to exclude a plot you may set -`plot.inspectable[] = false`. +Creates a data inspector which will show relevant information in a tooltip +when you hover over a plot. If you wish to exclude a plot you may set +`plot.inspectable[] = false`. ### Keyword Arguments: - `range = 10`: Controls the snapping range for selecting an element of a plot. - `enabled = true`: Disables inspection of plots when set to false. Can also be adjusted with `enable!(inspector)` and `disable!(inspector)`. -- `text_padding = Vec4f0(5, 5, 3, 3)`: Padding for the box drawn around the +- `text_padding = Vec4f0(5, 5, 3, 3)`: Padding for the box drawn around the tooltip text. (left, right, bottom, top) -- `text_align = (:left, :bottom)`: Alignment of text within the tooltip. This +- `text_align = (:left, :bottom)`: Alignment of text within the tooltip. This does not affect the alignment of the tooltip relative to the cursor. - `textcolor = :black`: Tooltip text color. - `textsize = 20`: Tooltip text size. @@ -414,14 +414,14 @@ when you hover over a plot. If you wish to exclude a plot you may set - `outline_linewidth = 2`: Linewidth of the tooltip outline. - `indicator_color = :red`: Color of the selection indicator. - `indicator_linewidth = 2`: Linewidth of the selection indicator. -- `indicator_linestyle = nothing`: Linestyle of the selection indicator +- `indicator_linestyle = nothing`: Linestyle of the selection indicator - `tooltip_align = (:center, :top)`: Default position of the tooltip relative to - the cursor or current selection. The real align may adjust to keep the + the cursor or current selection. The real align may adjust to keep the tooltip in view. -- `tooltip_offset = Vec2f0(20)`: Offset from the indicator to the tooltip. -- `depth = 9e3`: Depth value of the tooltip. This should be high so that the +- `tooltip_offset = Vec2f0(20)`: Offset from the indicator to the tooltip. +- `depth = 9e3`: Depth value of the tooltip. This should be high so that the tooltip is always in front. -- `priority = 100`: The priority of creating a tooltip on a mouse movement or +- `priority = 100`: The priority of creating a tooltip on a mouse movement or scrolling event. """ function DataInspector(fig_or_layoutable; kwargs...) @@ -433,8 +433,8 @@ function DataInspector(scene::Scene; priority = 100, kwargs...) @assert origin(pixelarea(parent)[]) == Vec2f0(0) plot = _inspector!( - parent, 1, - show_axis=false, _root_px_projection = camera(parent).pixel_space; + parent, 1, + show_axis=false, _root_px_projection = camera(parent).pixel_space; kwargs... ) inspector = DataInspector(parent, plot) @@ -549,14 +549,14 @@ end function show_data(inspector::DataInspector, plot::MeshScatter, idx) a = inspector.plot.attributes scene = parent_scene(plot) - + proj_pos = shift_project(scene, plot, to_ndim(Point3f0, plot[1][][idx], 0)) update_tooltip_alignment!(inspector, proj_pos) bbox = Rect{3, Float32}(plot.marker[]) a._model[] = transformationmatrix( plot[1][][idx], - to_scale(plot.markersize[], idx), + to_scale(plot.markersize[], idx), to_rotation(plot.rotations[], idx) ) @@ -570,7 +570,7 @@ function show_data(inspector::DataInspector, plot::MeshScatter, idx) clear_temporary_plots!(inspector, plot) p = wireframe!( - scene, a._bbox3D, model = a._model, color = a.indicator_color, + scene, a._bbox3D, model = a._model, color = a.indicator_color, linewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a._bbox_visible, show_axis = false, inspectable = false ) @@ -599,7 +599,7 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i origin, dir = view_ray(scene) pos = closest_point_on_line(p0, p1, origin, dir) lw = plot.linewidth[] isa Vector ? plot.linewidth[][idx] : plot.linewidth[] - + proj_pos = shift_project(scene, plot, to_ndim(Point3f0, pos, 0)) update_tooltip_alignment!(inspector, proj_pos) @@ -616,7 +616,7 @@ end function show_data(inspector::DataInspector, plot::Mesh, idx) a = inspector.plot.attributes scene = parent_scene(plot) - + bbox = boundingbox(plot) proj_pos = Point2f0(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) @@ -630,7 +630,7 @@ function show_data(inspector::DataInspector, plot::Mesh, idx) clear_temporary_plots!(inspector, plot) p = wireframe!( - scene, a._bbox3D, color = a.indicator_color, + scene, a._bbox3D, color = a.indicator_color, linewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a._bbox_visible, show_axis = false, inspectable = false ) @@ -653,7 +653,7 @@ end function show_data(inspector::DataInspector, plot::Surface, idx) a = inspector.plot.attributes scene = parent_scene(plot) - + proj_pos = Point2f0(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) @@ -727,7 +727,7 @@ function show_imagelike(inspector, plot, name, edge_based) a._color[] = if z isa AbstractFloat interpolated_getindex( - to_colormap(plot.colormap[]), z, + to_colormap(plot.colormap[]), z, to_value(get(plot.attributes, :colorrange, (0, 1))) ) else @@ -742,18 +742,18 @@ function show_imagelike(inspector, plot, name, edge_based) if inspector.selection != plot || !(inspector.temp_plots[1] isa Scatter) clear_temporary_plots!(inspector, plot) p = scatter!( - scene, map(p -> [p], a._position), color = a._color, + scene, map(p -> [p], a._position), color = a._color, visible = a._bbox_visible, - show_axis = false, inspectable = false, + show_axis = false, inspectable = false, marker=:rect, markersize = map(r -> 2r - 4, a.range), - strokecolor = a.indicator_color, + strokecolor = a.indicator_color, strokewidth = a.indicator_linewidth #, linestyle = a.indicator_linestyle no? ) translate!(p, Vec3f0(0, 0, a.depth[])) push!(inspector.temp_plots, p) # Hacky? push!( - inspector.obsfuncs, + inspector.obsfuncs, Observables.ObserverFunction(a._position.listeners[end], a._position, false) ) end @@ -763,7 +763,7 @@ function show_imagelike(inspector, plot, name, edge_based) if inspector.selection != plot || !(inspector.temp_plots[1][1][] isa Rect2D) clear_temporary_plots!(inspector, plot) p = wireframe!( - scene, a._bbox2D, model = a._model, color = a.indicator_color, + scene, a._bbox2D, model = a._model, color = a.indicator_color, strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a._bbox_visible, show_axis = false, inspectable = false ) @@ -787,9 +787,9 @@ function _interpolated_getindex(xs, ys, img, mpos) i = clamp((x - x0) / (x1 - x0) * size(img, 1) + 0.5, 1, size(img, 1)) j = clamp((y - y0) / (y1 - y0) * size(img, 2) + 0.5, 1, size(img, 2)) - l = clamp(floor(Int, i), 1, size(img, 1)-1); + l = clamp(floor(Int, i), 1, size(img, 1)-1); r = clamp(l+1, 2, size(img, 1)) - b = clamp(floor(Int, j), 1, size(img, 2)-1); + b = clamp(floor(Int, j), 1, size(img, 2)-1); t = clamp(b+1, 2, size(img, 2)) z = ((r-i) * img[l, b] + (i-l) * img[r, b]) * (t-j) + ((r-i) * img[l, t] + (i-l) * img[r, t]) * (j-b) @@ -870,7 +870,7 @@ end function show_data(inspector::DataInspector, plot::BarPlot, idx) a = inspector.plot.attributes scene = parent_scene(plot) - + pos = plot[1][][idx] proj_pos = shift_project(scene, plot, to_ndim(Point3f0, pos, 0)) update_tooltip_alignment!(inspector, proj_pos) @@ -880,7 +880,7 @@ function show_data(inspector::DataInspector, plot::BarPlot, idx) if inspector.selection != plot clear_temporary_plots!(inspector, plot) p = wireframe!( - scene, a._bbox2D, model = a._model, color = a.indicator_color, + scene, a._bbox2D, model = a._model, color = a.indicator_color, strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a._bbox_visible, show_axis = false, inspectable = false ) @@ -902,7 +902,7 @@ end function show_data(inspector::DataInspector, plot::Arrows, idx, source) a = inspector.plot.attributes scene = parent_scene(plot) - + pos = plot[1][][idx] proj_pos = shift_project(scene, plot, to_ndim(Point3f0, pos, 0)) @@ -937,7 +937,7 @@ end # What should this display? # function show_data( -# inspector::DataInspector, plot::Poly{<: Tuple{<: AbstractVector}}, +# inspector::DataInspector, plot::Poly{<: Tuple{<: AbstractVector}}, # idx, source::Mesh # ) # @info "PolyMesh" @@ -948,23 +948,23 @@ end function show_poly(inspector, plot, idx, source) a = inspector.plot.attributes scene = parent_scene(plot) - + idx = vertexindex2poly(plot[1][], idx) m = GeometryBasics.mesh(plot[1][][idx]) - + clear_temporary_plots!(inspector, plot) ext = plot[1][][idx].exterior p = lines!( - scene, ext, color = a.indicator_color, + scene, ext, color = a.indicator_color, strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a._visible, show_axis = false, inspectable = false ) translate!(p, Vec3f0(0,0,a.depth[])) push!(inspector.temp_plots, p) - + for int in plot[1][][idx].interiors p = lines!( - scene, int, color = a.indicator_color, + scene, int, color = a.indicator_color, strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a._visible, show_axis = false, inspectable = false ) diff --git a/src/interfaces.jl b/src/interfaces.jl index 130075c7bbd..711591c222a 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -1,9 +1,5 @@ not_implemented_for(x) = error("Not implemented for $(x). You might want to put: `using Makie` into your code!") -Attributes(x::AbstractPlot) = x.attributes - -default_theme(scene, T) = Attributes() - function default_theme(scene) Attributes( # color = theme(scene, :color), @@ -24,277 +20,6 @@ function default_theme(scene) ) end -""" - `calculated_attributes!(trait::Type{<: AbstractPlot}, plot)` -trait version of calculated_attributes -""" -calculated_attributes!(trait, plot) = nothing - -""" - `calculated_attributes!(plot::AbstractPlot)` -Fill in values that can only be calculated when we have all other attributes filled -""" -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). - -## Attributes -$(ATTRIBUTES) -""" -@recipe(Image, x, y, image) do scene - Attributes(; - default_theme(scene)..., - colormap = [:black, :white], - colorrange = automatic, - interpolate = true, - fxaa = false, - lowclip = nothing, - highclip = nothing, - inspectable = theme(scene, :inspectable) - ) -end - - -# could be implemented via image, but might be optimized specifically by the backend -""" - heatmap(x, y, values) - heatmap(values) - -Plots a heatmap as an image on `x, y` (defaults to interpretation as dimensions). - -## Attributes -$(ATTRIBUTES) -""" -@recipe(Heatmap, x, y, values) do scene - Attributes(; - default_theme(scene)..., - colormap = theme(scene, :colormap), - colorrange = automatic, - linewidth = 0.0, - interpolate = false, - levels = 1, - fxaa = true, - lowclip = nothing, - highclip = nothing, - inspectable = theme(scene, :inspectable) - ) -end - -""" - volume(volume_data) - -Plots a volume. Available algorithms are: -* `:iso` => IsoValue -* `:absorption` => Absorption -* `:mip` => MaximumIntensityProjection -* `:absorptionrgba` => AbsorptionRGBA -* `:additive` => AdditiveRGBA -* `:indexedabsorption` => IndexedAbsorptionRGBA - -## Attributes -$(ATTRIBUTES) -""" -@recipe(Volume, x, y, z, volume) do scene - Attributes(; - default_theme(scene)..., - algorithm = :mip, - isovalue = 0.5, - isorange = 0.05, - color = nothing, - colormap = theme(scene, :colormap), - colorrange = (0, 1), - fxaa = true, - inspectable = theme(scene, :inspectable) - ) -end - -""" - surface(x, y, 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. - -## Attributes -$(ATTRIBUTES) -""" -@recipe(Surface, x, y, z) do scene - Attributes(; - default_theme(scene)..., - color = nothing, - colormap = theme(scene, :colormap), - colorrange = automatic, - shading = true, - fxaa = true, - lowclip = nothing, - highclip = nothing, - invert_normals = false, - inspectable = theme(scene, :inspectable) - ) -end - -""" - lines(positions) - lines(x, y) - lines(x, y, z) - -Creates a connected line plot for each element in `(x, y, z)`, `(x, y)` or `positions`. - -!!! tip - You can separate segments by inserting `NaN`s. - -## Attributes -$(ATTRIBUTES) -""" -@recipe(Lines, positions) do scene - Attributes(; - default_theme(scene)..., - linewidth = theme(scene, :linewidth), - color = theme(scene, :linecolor), - colormap = theme(scene, :colormap), - colorrange = automatic, - linestyle = nothing, - fxaa = false, - cycle = [:color], - inspectable = theme(scene, :inspectable) - ) -end - -""" - linesegments(positions) - linesegments(x, y) - linesegments(x, y, z) - -Plots a line for each pair of points in `(x, y, z)`, `(x, y)`, or `positions`. - -## Attributes -$(ATTRIBUTES) -""" -@recipe(LineSegments, positions) do scene - default_theme(scene, Lines) -end - -# alternatively, mesh3d? Or having only mesh instead of poly + mesh and figure out 2d/3d via dispatch -""" - mesh(x, y, z) - mesh(mesh_object) - mesh(x, y, z, faces) - mesh(xyz, faces) - -Plots a 3D or 2D mesh. Supported `mesh_object`s include `Mesh` types from [GeometryBasics.jl](https://github.com/JuliaGeometry/GeometryBasics.jl). - -## Attributes -$(ATTRIBUTES) -""" -@recipe(Mesh, mesh) do scene - Attributes(; - default_theme(scene)..., - color = :black, - colormap = theme(scene, :colormap), - colorrange = automatic, - interpolate = false, - shading = true, - fxaa = true, - inspectable = theme(scene, :inspectable), - cycle = [:color => :patchcolor], - ) -end - -""" - scatter(positions) - scatter(x, y) - scatter(x, y, z) - -Plots a marker for each element in `(x, y, z)`, `(x, y)`, or `positions`. - -## Attributes -$(ATTRIBUTES) -""" -@recipe(Scatter, positions) do scene - Attributes(; - default_theme(scene)..., - color = theme(scene, :markercolor), - colormap = theme(scene, :colormap), - colorrange = automatic, - marker = theme(scene, :marker), - markersize = theme(scene, :markersize), - - strokecolor = theme(scene, :markerstrokecolor), - strokewidth = theme(scene, :markerstrokewidth), - glowcolor = RGBA(0, 0, 0, 0), - glowwidth = 0.0, - - rotations = Billboard(), - marker_offset = automatic, - transform_marker = false, # Applies the plots transformation to marker - uv_offset_width = Vec4f0(0), - distancefield = nothing, - markerspace = Pixel, - fxaa = false, - cycle = [:color], - inspectable = theme(scene, :inspectable) - ) -end - -""" - meshscatter(positions) - meshscatter(x, y) - meshscatter(x, y, z) - -Plots a mesh for each element in `(x, y, z)`, `(x, y)`, or `positions` (similar to `scatter`). -`markersize` is a scaling applied to the primitive passed as `marker`. - -## Attributes -$(ATTRIBUTES) -""" -@recipe(MeshScatter, positions) do scene - Attributes(; - default_theme(scene)..., - color = :black, - colormap = theme(scene, :colormap), - colorrange = automatic, - marker = Sphere(Point3f0(0), 1f0), - markersize = 0.1, - rotations = Quaternionf0(0, 0, 0, 1), - # markerspace = relative, - shading = true, - fxaa = true, - inspectable = theme(scene, :inspectable), - cycle = [:color], - ) -end - -""" - text(string) - -Plots a text. - -## Attributes -$(ATTRIBUTES) -""" -@recipe(Text, text) do scene - Attributes(; - default_theme(scene)..., - color = theme(scene, :textcolor), - font = theme(scene, :font), - strokecolor = (:black, 0.0), - strokewidth = 0, - align = (:left, :bottom), - rotation = 0.0, - textsize = 20, - position = Point2f0(0), - justification = automatic, - lineheight = 1.0, - space = :screen, # or :data - offset = Point2f0(0, 0), - _glyphlayout = nothing, - inspectable = theme(scene, :inspectable) - ) -end - function plot!(plot::Text) # attach a function to any text that calculates the glyph layout and stores it @@ -546,20 +271,6 @@ function (PlotType::Type{<: AbstractPlot{Typ}})(scene::SceneLike, attributes::At plot_obj end - - -""" - `plottype(plot_args...)` - -Any custom argument combination that has a preferred way to be plotted should overload this. -e.g.: -```example - # make plot(rand(5, 5, 5)) plot as a volume - plottype(x::Array{<: AbstractFloat, 3}) = Volume -``` -""" -plottype(plot_args...) = Combined{Any, Tuple{typeof.(to_value.(plot_args))...}} # default to dispatch to type recipes! - ## generic definitions # If the Combined has no plot func, calculate them plottype(::Type{<: Combined{Any}}, argvalues...) = plottype(argvalues...) @@ -597,20 +308,6 @@ end plottype(P1::Type{<: Combined{Any}}, P2::Type{<: Combined{T}}) where T = P2 plottype(P1::Type{<: Combined{T}}, P2::Type{<: Combined}) where T = P1 - -""" -Returns the Combined type that represents the signature of `args`. -""" -function Plot(args::Vararg{Any, N}) where N - Combined{Any, <: Tuple{args...}} -end -Base.@pure function Plot(::Type{T}) where T - Combined{Any, <: Tuple{T}} -end -Base.@pure function Plot(::Type{T1}, ::Type{T2}) where {T1, T2} - Combined{Any, <: Tuple{T1, T2}} -end - # all the plotting functions that get a plot type const PlotFunc = Union{Type{Any}, Type{<: AbstractPlot}} @@ -650,20 +347,12 @@ function plot!(P::PlotFunc, scene::SceneLike, attrs::Attributes, args...; kw_att 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)) - # plots to scene -plotfunc(::Combined{F}) where F = F - """ Main plotting signatures that plot/plot! route to if no Plot Type is given """ -function plot!(scene::SceneLike, P::PlotFunc, attributes::Attributes, args...; kw_attributes...) +function plot!(scene::Union{Combined, SceneLike}, P::PlotFunc, attributes::Attributes, args...; kw_attributes...) attributes = merge!(Attributes(kw_attributes), attributes) argvalues = to_value.(args) PreType = plottype(P, argvalues...) @@ -807,18 +496,9 @@ function plot!(scene::SceneLike, P::PlotFunc, attributes::Attributes, input::NTu return plot_object end -function plot!(scene::Combined, P::PlotFunc, attributes::Attributes, args...) - # create "empty" plot type - empty meaning containing no plots, just attributes + arguments - constr = plottype(P, args...) - plot_object = constr(scene, attributes, args) - # call user defined recipe overload to fill the plot type - plot!(plot_object) - push!(scene.plots, plot_object) - plot_object -end - function plot!(scene::Combined, P::PlotFunc, attributes::Attributes, input::NTuple{N,Node}, args::Node) 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) diff --git a/src/makielayout/MakieLayout.jl b/src/makielayout/MakieLayout.jl index ae86639f5b8..546c125b5fc 100644 --- a/src/makielayout/MakieLayout.jl +++ b/src/makielayout/MakieLayout.jl @@ -8,7 +8,8 @@ using ..Makie.Mouse using ..Makie: ispressed, is_mouseinside, get_scene, FigureLike using ..Makie: _sanitize_observer_function using ..Makie: OpenInterval, Interval -using ..Makie: Automatic, automatic +using MakieCore +using MakieCore: Automatic, automatic using Observables: onany import Observables import Formatting diff --git a/src/makielayout/layoutables/axis.jl b/src/makielayout/layoutables/axis.jl index 96a924a92b7..dc57374b6bc 100644 --- a/src/makielayout/layoutables/axis.jl +++ b/src/makielayout/layoutables/axis.jl @@ -580,7 +580,7 @@ function get_cycler_index!(c::Cycler, P::Type) end function get_cycle_for_plottype(allattrs, P)::Cycle - psym = Makie.plotsym(P) + psym = MakieCore.plotsym(P) plottheme = Makie.default_theme(nothing, P) diff --git a/src/makielayout/layoutables/legend.jl b/src/makielayout/layoutables/legend.jl index 705c72bd957..8f72a6434ad 100644 --- a/src/makielayout/layoutables/legend.jl +++ b/src/makielayout/layoutables/legend.jl @@ -514,7 +514,7 @@ function layoutable(::Type{Legend}, fig_or_scene, error("Number of elements not equal: $(length(titles)) titles, $(length(contentgroups)) content groups and $(length(labelgroups)) label groups.") end - + entrygroups = Node{Vector{EntryGroup}}([]) legend = layoutable(Legend, fig_or_scene, entrygroups; kwargs...) entries = [[LegendEntry(l, pg, legend) for (l, pg) in zip(labelgroup, contentgroup)] diff --git a/src/precompile.jl b/src/precompile.jl index 72bbfcffde7..26362970f89 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -122,15 +122,12 @@ function _precompile_() precompile(fbody, (Tuple{Tuple{Float32, Float32}, Tuple{Float32, Float32}},Type,Symbol,typeof(lift),Function,Observable{Tuple{Tuple{Tuple{Float32, Float32}, Tuple{Float32, Float32}}}},)) end end # time: 0.00889073 - Base.precompile(Tuple{typeof(value_convert),Tuple{Tuple{Bool, Bool}, Tuple{Bool, Bool}}}) # time: 0.008890678 - Base.precompile(Tuple{typeof(value_convert),Tuple{Makie.Keyboard.Button, Makie.Mouse.Button}}) # time: 0.008794897 let fbody = try Base.bodyfunction(which(lift, (Function,Observable{Tuple{Vector{Point{2, Float32}}}},))) catch missing end if !ismissing(fbody) precompile(fbody, (Vector{Point{2, Float32}},Type,Symbol,typeof(lift),Function,Observable{Tuple{Vector{Point{2, Float32}}}},)) end end # time: 0.00867902 Base.precompile(Tuple{typeof(default_theme),Scene,Type{Lines{Tuple{Vector{Point{2, Float32}}}}}}) # time: 0.008428088 - Base.precompile(Tuple{typeof(value_convert),Tuple{Tuple{Symbol, Symbol}, Tuple{Symbol, Symbol}}}) # time: 0.008128306 let fbody = try Base.bodyfunction(which(lift, (Function,Observable{GeometryBasics.HyperRectangle{2, Float32}},))) catch missing end if !ismissing(fbody) precompile(fbody, (Tuple{GeometryBasics.HyperRectangle{2, Float32}},Type,Symbol,typeof(lift),Function,Observable{GeometryBasics.HyperRectangle{2, Float32}},)) @@ -147,7 +144,6 @@ function _precompile_() Base.precompile(Tuple{typeof(setindex!),LineSegments{Tuple{Vector{Point{2, Float32}}}},Observable{Vector{RGBA{Float32}}},Symbol}) # time: 0.006341601 Base.precompile(Tuple{typeof(convert_attribute),Vector{Float32},Key{:textsize}}) # time: 0.006148856 isdefined(Makie, Symbol("#634#635")) && Base.precompile(Tuple{getfield(Makie, Symbol("#634#635")),Vec{3, Float32},Vec{3, Float32},Quaternionf0,Vec{2, Float32},SMatrix{4, 4, Float32, 16},Tuple{Bool, Bool, Bool}}) # time: 0.006117055 - Base.precompile(Tuple{typeof(value_convert),Tuple{Nothing, Nothing}}) # time: 0.005686621 Base.precompile(Tuple{typeof(default_labels),Automatic,Tuple{Vector{Float64}, Vector{Float64}},Function}) # time: 0.005572814 Base.precompile(Tuple{Core.kwftype(typeof(plot!)),NamedTuple{(:align, :model, :position, :color, :visible, :textsize, :font, :rotation), Tuple{Vec{2, Float32}, SMatrix{4, 4, Float32, 16}, Vector{Point{3, Float32}}, Vector{RGBA{Float32}}, Observable{Any}, Vector{Float32}, Vector{FTFont}, Vector{Quaternionf0}}},typeof(plot!),Type{Text{ArgType} where ArgType},Annotations{Tuple{Vector{Tuple{String, Point{2, Float32}}}}},String}) # time: 0.005202268 let fbody = try Base.bodyfunction(which(lift, (Function,Observable{GeometryBasics.HyperRectangle{2, Int64}},))) catch missing end diff --git a/src/scenes.jl b/src/scenes.jl index 6ad84396cbc..8d0f7e4d08c 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -320,19 +320,6 @@ function getindex(scene::Scene, ::Type{OldAxis}) nothing end - -""" -Each argument can be named for a certain plot type `P`. Falls back to `arg1`, `arg2`, etc. -""" -function argument_names(plot::P) where P <: AbstractPlot - argument_names(P, length(plot.converted)) -end - -function argument_names(::Type{<: AbstractPlot}, num_args::Integer) - # this is called in the indexing function, so let's be a bit efficient - ntuple(i -> Symbol("arg$i"), num_args) -end - function Base.empty!(scene::Scene) empty!(scene.plots) disconnect!(scene.camera) @@ -356,13 +343,6 @@ function scene_limits(scene::Scene) end end -# Since we can use Combined like a scene in some circumstances, we define this alias -theme(x::SceneLike, args...) = theme(x.parent, args...) -theme(x::Scene) = x.theme -theme(x::Scene, key) = deepcopy(x.theme[key]) -theme(x::AbstractPlot, key) = deepcopy(x.attributes[key]) -theme(::Nothing, key::Symbol) = deepcopy(current_default_theme()[key]) - Base.push!(scene::Combined, subscene) = nothing # Combined plots add themselves uppon creation function Base.push!(scene::Scene, plot::AbstractPlot) diff --git a/src/theming.jl b/src/theming.jl index 6f69db62f20..04b1ecccbb1 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -147,6 +147,8 @@ function with_theme(f, theme = Theme(); kwargs...) end end +theme(::Nothing, key::Symbol) = deepcopy(current_default_theme()[key]) + """ update_theme!(with_theme::Theme; kwargs...) diff --git a/src/types.jl b/src/types.jl index 3ac9336e2e6..72b4c2a13ac 100644 --- a/src/types.jl +++ b/src/types.jl @@ -19,8 +19,8 @@ include("interaction/iodevices.jl") This struct provides accessible `PriorityObservable`s to monitor the events associated with a Scene. -Functions that act on a `PriorityObservable` must return true if the function -consumes an event and false if it does not. When an event is consumed it does +Functions that act on a `PriorityObservable` must return true if the function +consumes an event and false if it does not. When an event is consumed it does not trigger other observer functions. The order in which functions are exectued can be controlled via the `priority` keyword (default 0) in `on`. @@ -53,7 +53,7 @@ struct Events window_open::PriorityObservable{Bool} """ - Most recently triggered `MouseButtonEvent`. Contains the relevant + Most recently triggered `MouseButtonEvent`. Contains the relevant `event.button` and `event.action` (press/release) See also [`ispressed`](@ref). @@ -118,7 +118,7 @@ function Events() # This never consumes because it just keeps track of the state return false end - + keyboardbutton = PriorityObservable(KeyEvent(Keyboard.unknown, Keyboard.release)) keyboardstate = Set{Keyboard.Button}() on(keyboardbutton, priority = typemax(Int8)) do event @@ -229,7 +229,6 @@ function Base.getproperty(e::Events, field::Symbol) end end - mutable struct Camera pixel_space::Node{Mat4f0} view::Node{Mat4f0} @@ -242,7 +241,6 @@ end """ Holds the transformations for Scenes. - ## Fields $(TYPEDFIELDS) """ @@ -264,62 +262,6 @@ struct Transformation <: Transformable end end -struct Combined{Typ, T} <: ScenePlot{Typ} - parent::SceneLike - transformation::Transformation - attributes::Attributes - input_args::Tuple - converted::Tuple - plots::Vector{AbstractPlot} -end - -function Base.show(io::IO, plot::Combined) - print(io, typeof(plot)) -end - -parent(x::AbstractPlot) = x.parent - -function func2string(func::F) where F <: Function - string(F.name.mt.name) -end - -plotkey(::Type{<: AbstractPlot{Typ}}) where Typ = Symbol(lowercase(func2string(Typ))) -plotkey(::T) where T <: AbstractPlot = plotkey(T) - -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} - - -""" - Billboard([angle::Real]) - Billboard([angles::Vector{<: Real}]) - -Billboard attribute to always have a primitive face the camera. -Can be used for rotation. -""" -struct Billboard{T <: Union{Float32, Vector{Float32}}} - rotation::T -end -Billboard() = Billboard(0f0) -Billboard(angle::Real) = Billboard(Float32(angle)) -Billboard(angles::Vector) = Billboard(Float32.(angles)) - -""" -Type to indicate that an attribute will get calculated automatically -""" -struct Automatic end - -""" -Singleton instance to indicate that an attribute will get calculated automatically -""" -const automatic = Automatic() - - """ `PlotSpec{P<:AbstractPlot}(args...; kwargs...)` diff --git a/src/units.jl b/src/units.jl index 6da14acd567..00efbe04fe4 100644 --- a/src/units.jl +++ b/src/units.jl @@ -21,8 +21,6 @@ function to_screen(scene::Scene, mpos) return Point2f0(mpos) .- Point2f0(minimum(pixelarea(scene)[])) end -abstract type Unit{T} <: Number end - number(x::Unit) = x.value number(x) = x @@ -55,25 +53,12 @@ const dip = DIP(1) const dip_in_millimeter = 0.15875 const dip_in_inch = 1/160 -""" -Unit in pixels on screen. -This one is a bit tricky, since it refers to a static attribute (pixels on screen don't change) -but since every visual is attached to a camera, the exact scale might change. -So in the end, this is just relative to some normed camera - the value on screen, depending on the camera, -will not actually sit on those pixels. Only camera that guarantees the correct mapping is the -`:pixel` camera type. -""" -struct Pixel{T} <: Unit{T} - value::T -end basetype(::Type{<: Pixel}) = Pixel -const px = Pixel(1) - """ Millimeter on screen. This unit respects the dimension and pixel density of the screen to represent millimeters on the screen. This is the must use unit for layouting, -that needs to look the same on all kind of screens. Similar as with the [`Pixel`](@ref) unit, +that needs to look the same on all kind of screens. Similar as with the `Pixel` unit, a camera can change the actually displayed dimensions of any object using the millimeter unit. """ struct Millimeter{T} <: Unit{T} diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index c213b9ad8f1..5ba846d9c17 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -227,12 +227,6 @@ function merged_get!(defaults::Function, key, scene::SceneLike, input::Attribute return merge!(input, d) end -struct Key{K} end -macro key_str(arg) - :(Key{$(QuoteNode(Symbol(arg)))}) -end -Base.broadcastable(x::Key) = (x,) - 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 diff --git a/test/Project.toml b/test/Project.toml index 85e1f7d8ba5..321e462fb91 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,7 +2,6 @@ CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" -ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" diff --git a/test/unit_tests/conversions.jl b/test/conversions.jl similarity index 100% rename from test/unit_tests/conversions.jl rename to test/conversions.jl diff --git a/test/unit_tests/events.jl b/test/events.jl similarity index 100% rename from test/unit_tests/events.jl rename to test/events.jl diff --git a/test/unit_tests/figures.jl b/test/figures.jl similarity index 100% rename from test/unit_tests/figures.jl rename to test/figures.jl diff --git a/test/unit_tests/liftmacro.jl b/test/liftmacro.jl similarity index 100% rename from test/unit_tests/liftmacro.jl rename to test/liftmacro.jl diff --git a/test/unit_tests/makielayout.jl b/test/makielayout.jl similarity index 100% rename from test/unit_tests/makielayout.jl rename to test/makielayout.jl diff --git a/test/unit_tests/projection_math.jl b/test/projection_math.jl similarity index 100% rename from test/unit_tests/projection_math.jl rename to test/projection_math.jl diff --git a/test/unit_tests/quaternions.jl b/test/quaternions.jl similarity index 100% rename from test/unit_tests/quaternions.jl rename to test/quaternions.jl diff --git a/test/reference_tests.jl b/test/reference_tests.jl deleted file mode 100644 index 928e36fb3a2..00000000000 --- a/test/reference_tests.jl +++ /dev/null @@ -1,11 +0,0 @@ -Pkg.develop(Pkg.PackageSpec(path=joinpath(@__DIR__, "ReferenceTests"))) - -using ReferenceTests -using ReferenceTests: @cell - -db = ReferenceTests.load_database() -ReferenceTests.record_tests(db) -# needs GITHUB_TOKEN to be set: -# ReferenceTests.upload_reference_images() -# Needs a backend to actually have something recoreded: -# ReferenceTests.reference_tests(recorded) diff --git a/test/runtests.jl b/test/runtests.jl index 1de6f734a89..490a5249141 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,10 +1,6 @@ -using Pkg using Test -using MeshIO using StaticArrays using Makie -using ImageMagick - using Makie.Observables using Makie.GeometryBasics using Makie.PlotUtils @@ -12,10 +8,24 @@ using Makie.FileIO using Makie.IntervalSets using GeometryBasics: Pyramid -# ImageIO seems broken on 1.6 ... and there doesn't -# seem to be a clean way anymore to force not to use a loader library? -filter!(x-> x !== :ImageIO, FileIO.sym2saver[:PNG]) -filter!(x-> x !== :ImageIO, FileIO.sym2loader[:PNG]) +using Makie: volume + +@testset "Unit tests" begin + @testset "#659 Volume errors if data is not a cube" begin + fig, ax, vplot = volume(1:8, 1:8, 1:10, rand(8, 8, 10)) + lims = Makie.data_limits(vplot) + lo, hi = extrema(lims) + @test all(lo .<= 1) + @test all(hi .>= (8,8,10)) + end -include("reference_tests.jl") -include("unit_tests/runtests.jl") + include("conversions.jl") + include("quaternions.jl") + include("projection_math.jl") + include("liftmacro.jl") + include("makielayout.jl") + include("figures.jl") + include("transformations.jl") + include("stack.jl") + include("events.jl") +end diff --git a/test/unit_tests/stack.jl b/test/stack.jl similarity index 100% rename from test/unit_tests/stack.jl rename to test/stack.jl diff --git a/test/unit_tests/statistical_tests.jl b/test/statistical_tests.jl similarity index 100% rename from test/unit_tests/statistical_tests.jl rename to test/statistical_tests.jl diff --git a/test/unit_tests/transformations.jl b/test/transformations.jl similarity index 100% rename from test/unit_tests/transformations.jl rename to test/transformations.jl diff --git a/test/unit_tests/runtests.jl b/test/unit_tests/runtests.jl deleted file mode 100644 index 9d00e69a790..00000000000 --- a/test/unit_tests/runtests.jl +++ /dev/null @@ -1,25 +0,0 @@ -using Makie: volume - -@testset "Unit tests" begin - @testset "#659 Volume errors if data is not a cube" begin - fig, ax, vplot = volume(1:8, 1:8, 1:10, rand(8, 8, 10)) - lims = Makie.data_limits(vplot) - lo, hi = extrema(lims) - @test all(lo .<= 1) - @test all(hi .>= (8,8,10)) - end - - include("conversions.jl") - include("quaternions.jl") - include("projection_math.jl") - include("liftmacro.jl") - include("makielayout.jl") - include("figures.jl") - include("transformations.jl") - include("stack.jl") - include("events.jl") - - # Skipped: - # include("zoom_pan.jl") - # include("statistical_tests.jl") -end diff --git a/test/unit_tests/zoom_pan.jl b/test/zoom_pan.jl similarity index 100% rename from test/unit_tests/zoom_pan.jl rename to test/zoom_pan.jl