diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index 621fc253bfb..d82d780f389 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -62,7 +62,8 @@ excludes = Set([ "Connected Sphere", # markers too big, close otherwise, needs to be assimilated with glmakie "Unicode Marker", - "Depth Shift" + "Depth Shift", + "Order Independent Transparency" ]) excludes2 = Set(["short_tests_90", "short_tests_111", "short_tests_35", "short_tests_13", "short_tests_3"]) diff --git a/GLMakie/assets/shader/fragment_output.frag b/GLMakie/assets/shader/fragment_output.frag index f334cbcb328..9386137c0ad 100644 --- a/GLMakie/assets/shader/fragment_output.frag +++ b/GLMakie/assets/shader/fragment_output.frag @@ -3,17 +3,38 @@ layout(location=0) out vec4 fragment_color; layout(location=1) out uvec2 fragment_groupid; {{buffers}} +// resolves to: +// // if transparency == true +// layout(location=2) out float coverage; +// // if transparency == false && enable_SSAO[] = true +// layout(location=2) out vec3 fragment_position; +// layout(location=3) out vec3 fragment_normal_occlusion; -in vec4 o_view_pos; + +in vec3 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}} + // resolves to: + + // // if transparency == true + // float weight = color.a * max(0.01, 3000 * pow((1 - gl_FragCoord.z), 3)); + // fragment_color = weight * color; + // coverage = color.a; + + // // if transparency == false && enable_SSAO[] = true + // fragment_color = color; + // fragment_position = o_view_pos; + // fragment_normal_occlusion.xyz = o_normal; + + // // else + // fragment_color = color; } diff --git a/GLMakie/assets/shader/heatmap.vert b/GLMakie/assets/shader/heatmap.vert index 3399b10c69c..df15cab1327 100644 --- a/GLMakie/assets/shader/heatmap.vert +++ b/GLMakie/assets/shader/heatmap.vert @@ -12,7 +12,7 @@ uniform float depth_shift; out vec2 o_uv; flat out uvec2 o_objectid; -out vec4 o_view_pos; +out vec3 o_view_pos; out vec3 o_normal; ivec2 ind2sub(ivec2 dim, int linearindex){ @@ -21,7 +21,7 @@ ivec2 ind2sub(ivec2 dim, int linearindex){ void main(){ //Outputs for ssao, which we don't use for 2d shaders like heatmap/image - o_view_pos = vec4(0); + o_view_pos = vec3(0); o_normal = vec3(0); int index = gl_InstanceID; diff --git a/GLMakie/assets/shader/image.vert b/GLMakie/assets/shader/image.vert index 9d08024901c..9132f39efcf 100644 --- a/GLMakie/assets/shader/image.vert +++ b/GLMakie/assets/shader/image.vert @@ -11,7 +11,7 @@ uniform float depth_shift; out vec2 o_uv; flat out uvec2 o_objectid; -out vec4 o_view_pos; +out vec3 o_view_pos; out vec3 o_normal; vec4 _position(vec3 p){return vec4(p,1);} @@ -19,7 +19,7 @@ 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_view_pos = vec3(0); o_normal = vec3(0); o_uv = texturecoordinates; o_objectid = uvec2(objectid, gl_VertexID+1); diff --git a/GLMakie/assets/shader/line_segment.geom b/GLMakie/assets/shader/line_segment.geom index 1a7b232081d..ace89522e6d 100644 --- a/GLMakie/assets/shader/line_segment.geom +++ b/GLMakie/assets/shader/line_segment.geom @@ -38,12 +38,12 @@ void emit_vertex(vec2 position, vec2 uv, int index) uniform int max_primtives; -out vec4 o_view_pos; +out vec3 o_view_pos; out vec3 o_normal; void main(void) { - o_view_pos = vec4(0); + o_view_pos = vec3(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 diff --git a/GLMakie/assets/shader/lines.geom b/GLMakie/assets/shader/lines.geom index 85b98cacf94..8c43f85424f 100644 --- a/GLMakie/assets/shader/lines.geom +++ b/GLMakie/assets/shader/lines.geom @@ -44,12 +44,12 @@ void emit_vertex(vec2 position, vec2 uv, int index, float ratio) uniform int max_primtives; const float infinity = 1.0 / 0.0; -out vec4 o_view_pos; +out vec3 o_view_pos; out vec3 o_normal; void main(void) { - o_view_pos = vec4(0); + o_view_pos = vec3(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 diff --git a/GLMakie/assets/shader/postprocessing/OIT_blend.frag b/GLMakie/assets/shader/postprocessing/OIT_blend.frag new file mode 100644 index 00000000000..a8ac6dc08e1 --- /dev/null +++ b/GLMakie/assets/shader/postprocessing/OIT_blend.frag @@ -0,0 +1,25 @@ +{{GLSL_VERSION}} + +// Based on https://jcgt.org/published/0002/02/09/ +// See https://github.com/JuliaPlots/Makie.jl/issues/1390 + +in vec2 frag_uv; + +// contains sum_i C_i * weight(depth_i, alpha_i) +uniform sampler2D sum_color; +// contains pod_i (1 - alpha_i) +uniform sampler2D prod_alpha; + +out vec4 fragment_color; + +void main(void) +{ + vec4 summed_color_weight = texture(sum_color, frag_uv); + float transmittance = texture(prod_alpha, frag_uv).r; + + vec3 weighted_transparent = summed_color_weight.rgb / max(summed_color_weight.a, 0.00001); + vec3 full_weighted_transparent = weighted_transparent * (1 - transmittance); + + fragment_color.rgb = full_weighted_transparent; + fragment_color.a = transmittance; +} diff --git a/GLMakie/assets/shader/sprites.geom b/GLMakie/assets/shader/sprites.geom index f384de75a24..d8b91b3148e 100644 --- a/GLMakie/assets/shader/sprites.geom +++ b/GLMakie/assets/shader/sprites.geom @@ -97,12 +97,12 @@ mat2 diagm(vec2 v){ return mat2(v.x, 0.0, 0.0, v.y); } -out vec4 o_view_pos; +out vec3 o_view_pos; out vec3 o_normal; void main(void) { - o_view_pos = vec4(0); + o_view_pos = vec3(0); o_normal = vec3(0); // emit quad as triangle strip diff --git a/GLMakie/assets/shader/util.vert b/GLMakie/assets/shader/util.vert index 1ccd22a7a71..c22f878776f 100644 --- a/GLMakie/assets/shader/util.vert +++ b/GLMakie/assets/shader/util.vert @@ -225,7 +225,7 @@ vec4 _color(Nothing color, float intensity, sampler1D color_map, vec2 color_norm return color_lookup(intensity, color_map, color_norm); } -out vec4 o_view_pos; +out vec3 o_view_pos; out vec3 o_normal; out vec3 o_lightdir; out vec3 o_camdir; @@ -242,17 +242,18 @@ void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 l // normal in world space o_normal = normalmatrix * normal; // position in view space (as seen from camera) - o_view_pos = view * position_world; + vec4 view_pos = view * position_world; // position in clip space (w/ depth) - gl_Position = projection * o_view_pos; + gl_Position = projection * view_pos; gl_Position.z += gl_Position.w * depth_shift; // direction to light - o_lightdir = normalize(view*vec4(lightposition, 1.0) - o_view_pos).xyz; + o_lightdir = normalize(view*vec4(lightposition, 1.0) - view_pos).xyz; // direction to camera // This is equivalent to - // normalize(view*vec4(eyeposition, 1.0) - o_view_pos).xyz + // normalize(view*vec4(eyeposition, 1.0) - view_pos).xyz // (by definition `view * eyeposition = 0`) - o_camdir = normalize(-o_view_pos).xyz; + o_camdir = normalize(-view_pos).xyz; + o_view_pos = view_pos.xyz / view_pos.w; } // diff --git a/GLMakie/assets/shader/volume.vert b/GLMakie/assets/shader/volume.vert index 37736336017..72c60a336c8 100644 --- a/GLMakie/assets/shader/volume.vert +++ b/GLMakie/assets/shader/volume.vert @@ -10,13 +10,13 @@ uniform vec3 lightposition; uniform mat4 modelinv; uniform float depth_shift; -out vec4 o_view_pos; +out vec3 o_view_pos; out vec3 o_normal; void main() { // TODO set these in volume.frag - o_view_pos = vec4(0); + o_view_pos = vec3(0); o_normal = vec3(0); vec4 world_vert = model * vec4(vertices, 1); frag_vert = world_vert.xyz; diff --git a/GLMakie/src/GLAbstraction/GLRender.jl b/GLMakie/src/GLAbstraction/GLRender.jl index 0c4bb247a47..f88eff5d14e 100644 --- a/GLMakie/src/GLAbstraction/GLRender.jl +++ b/GLMakie/src/GLAbstraction/GLRender.jl @@ -156,6 +156,10 @@ end function enabletransparency() glEnablei(GL_BLEND, 0) glDisablei(GL_BLEND, 1) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + # This does: + # target.rgb = source.a * source.rgb + (1 - source.a) * target.rgb + # target.a = 0 * source.a + 1 * target.a + # the latter is required to keep target.a = 1 for the OIT pass + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE) return end diff --git a/GLMakie/src/GLAbstraction/GLRenderObject.jl b/GLMakie/src/GLAbstraction/GLRenderObject.jl index 8f95cc485c9..475c7988990 100644 --- a/GLMakie/src/GLAbstraction/GLRenderObject.jl +++ b/GLMakie/src/GLAbstraction/GLRenderObject.jl @@ -42,12 +42,34 @@ function (sp::StandardPrerender)() 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() + + if sp.transparency[] + # disable depth buffer writing + glDepthMask(GL_FALSE) + + # Blending + glEnable(GL_BLEND) + glBlendEquation(GL_FUNC_ADD) + + # buffer 0 contains weight * color.rgba, should do sum + # destination <- 1 * source + 1 * destination + glBlendFunci(0, GL_ONE, GL_ONE) + + # buffer 1 is objectid, do nothing + glDisablei(1, GL_BLEND) + + # buffer 2 is color.a, should do product + # destination <- 0 * source + (1 - source) * destination + glBlendFunci(2, GL_ZERO, GL_ONE_MINUS_SRC_COLOR) + + else + glDepthMask(GL_TRUE) + enabletransparency() + end end struct StandardPostrender diff --git a/GLMakie/src/GLVisualize/visualize/image_like.jl b/GLMakie/src/GLVisualize/visualize/image_like.jl index 558e09e6063..152088a50b3 100644 --- a/GLMakie/src/GLVisualize/visualize/image_like.jl +++ b/GLMakie/src/GLVisualize/visualize/image_like.jl @@ -28,8 +28,15 @@ function _default(main::MatTypes{T}, ::Style, data::Dict) where T <: Colorant 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)")) + transparency = false + shader = GLVisualizeShader( + "fragment_output.frag", "image.vert", "texture.frag", + view = Dict( + "uv_swizzle" => "o_uv.$(spatialorder)", + "buffers" => output_buffers(to_value(transparency)), + "buffer_writes" => output_buffer_writes(to_value(transparency)) + ) + ) end end @@ -56,7 +63,14 @@ function gl_heatmap(main::MatTypes{T}, data::Dict) where T <: AbstractFloat 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") + transparency = false + shader = GLVisualizeShader( + "fragment_output.frag", "heatmap.vert", "intensity.frag", + view = Dict( + "buffers" => output_buffers(to_value(transparency)), + "buffer_writes" => output_buffer_writes(to_value(transparency)) + ) + ) fxaa = false end return data @@ -87,20 +101,23 @@ function _default(main::VolumeTypes{T}, s::Style, data::Dict) where T <: VolumeE model = Mat4f(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 + 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 enable_depth = true + transparency = false shader = GLVisualizeShader( "fragment_output.frag", "util.vert", "volume.vert", "volume.frag", view = Dict( "depth_init" => vol_depth_init(to_value(enable_depth)), "depth_main" => vol_depth_main(to_value(enable_depth)), - "depth_write" => vol_depth_write(to_value(enable_depth)) + "depth_write" => vol_depth_write(to_value(enable_depth)), + "buffers" => output_buffers(to_value(transparency)), + "buffer_writes" => output_buffer_writes(to_value(transparency)) ) ) prerender = VolumePrerender(data[:transparency], data[:overdraw]) @@ -136,7 +153,14 @@ function _default(main::VolumeTypes{T}, s::Style, data::Dict) where T <: RGBA color = color_map === nothing ? default(RGBA, s) : nothing algorithm = AbsorptionRGBA - shader = GLVisualizeShader("fragment_output.frag", "util.vert", "volume.vert", "volume.frag") + transparency = false + shader = GLVisualizeShader( + "fragment_output.frag", "util.vert", "volume.vert", "volume.frag", + view = Dict( + "buffers" => output_buffers(to_value(transparency)), + "buffer_writes" => output_buffer_writes(to_value(transparency)) + ) + ) prerender = VolumePrerender(data[:transparency], data[:overdraw]) postrender = () -> glDisable(GL_CULL_FACE) end diff --git a/GLMakie/src/GLVisualize/visualize/lines.jl b/GLMakie/src/GLVisualize/visualize/lines.jl index 9eed6231809..dbe4055f726 100644 --- a/GLMakie/src/GLVisualize/visualize/lines.jl +++ b/GLMakie/src/GLVisualize/visualize/lines.jl @@ -79,7 +79,14 @@ function line_visualization(position::Union{VectorTypes{T}, MatTypes{T}}, data:: len0 = length(p) - 1 return isempty(p) ? Cuint[] : Cuint[0; 0:len0; len0] end => to_index_buffer - shader = GLVisualizeShader("fragment_output.frag", "util.vert", "lines.vert", "lines.geom", "lines.frag") + transparency = false + shader = GLVisualizeShader( + "fragment_output.frag", "util.vert", "lines.vert", "lines.geom", "lines.frag", + view = Dict( + "buffers" => output_buffers(to_value(transparency)), + "buffer_writes" => output_buffer_writes(to_value(transparency)) + ) + ) gl_primitive = GL_LINE_STRIP_ADJACENCY valid_vertex = const_lift(p_vec) do points map(p-> Float32(all(isfinite, p)), points) @@ -115,7 +122,14 @@ function _default(positions::VectorTypes{T}, s::style"linesegment", data::Dict) 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") + transparency = false + shader = GLVisualizeShader( + "fragment_output.frag", "util.vert", "line_segment.vert", "line_segment.geom", "lines.frag", + view = Dict( + "buffers" => output_buffers(to_value(transparency)), + "buffer_writes" => output_buffer_writes(to_value(transparency)) + ) + ) gl_primitive = GL_LINES end if !isa(pattern, Texture) && pattern != nothing diff --git a/GLMakie/src/GLVisualize/visualize/mesh.jl b/GLMakie/src/GLVisualize/visualize/mesh.jl index 9e41fbad83b..aeec271bc0b 100644 --- a/GLMakie/src/GLVisualize/visualize/mesh.jl +++ b/GLMakie/src/GLVisualize/visualize/mesh.jl @@ -12,9 +12,14 @@ function _default(mesh::TOrSignal{M}, s::Style, data::Dict) where M <: GeometryB color_norm = nothing fetch_pixel = false uv_scale = Vec2f(1) + transparency = false shader = GLVisualizeShader( - "fragment_output.frag", "util.vert", "standard.vert", "standard.frag", - view = Dict("light_calc" => light_calc(shading)) + "util.vert", "standard.vert", "standard.frag", "fragment_output.frag", + view = Dict( + "light_calc" => light_calc(shading), + "buffers" => output_buffers(to_value(transparency)), + "buffer_writes" => output_buffer_writes(to_value(transparency)) + ) ) end end diff --git a/GLMakie/src/GLVisualize/visualize/particles.jl b/GLMakie/src/GLVisualize/visualize/particles.jl index 0cfb23e41f6..617b39eb14a 100644 --- a/GLMakie/src/GLVisualize/visualize/particles.jl +++ b/GLMakie/src/GLVisualize/visualize/particles.jl @@ -105,11 +105,14 @@ function meshparticle(p, s, data) instances = const_lift(length, position) shading = true backlight = 0f0 + transparency = false shader = GLVisualizeShader( - "util.vert", "particles.vert", "fragment_output.frag", "standard.frag", + "util.vert", "particles.vert", "standard.frag", "fragment_output.frag", view = Dict( "position_calc" => position_calc(position, position_x, position_y, position_z, TextureBuffer), - "light_calc" => light_calc(shading) + "light_calc" => light_calc(shading), + "buffers" => output_buffers(to_value(transparency)), + "buffer_writes" => output_buffer_writes(to_value(transparency)) ) ) end @@ -140,10 +143,17 @@ function _default(position::VectorTypes{T}, s::style"speed", data::Dict) where T @gen_defaults! data begin vertex = position => GLBuffer color_map = nothing => Texture - color = (color_map == nothing ? default(RGBA{Float32}, s) : nothing) => GLBuffer + color = (color_map === nothing ? default(RGBA{Float32}, s) : nothing) => GLBuffer color_norm = nothing scale = 2f0 - shader = GLVisualizeShader("fragment_output.frag", "dots.vert", "dots.frag") + transparency = false + shader = GLVisualizeShader( + "fragment_output.frag", "dots.vert", "dots.frag", + view = Dict( + "buffers" => output_buffers(to_value(transparency)), + "buffer_writes" => output_buffer_writes(to_value(transparency)) + ) + ) gl_primitive = GL_POINTS end data[:prerender] = PointSizeRender(data[:scale]) @@ -309,10 +319,15 @@ function sprites(p, s, data) # rotation and billboard don't go along billboard = rotation == Vec4f(0,0,0,1) => "if `billboard` == true, particles will always face camera" fxaa = false + transparency = 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)) + view = Dict( + "position_calc" => position_calc(position, position_x, position_y, position_z, GLBuffer), + "buffers" => output_buffers(to_value(transparency)), + "buffer_writes" => output_buffer_writes(to_value(transparency)) + ) ) scale_primitive = true gl_primitive = GL_POINTS diff --git a/GLMakie/src/GLVisualize/visualize/surface.jl b/GLMakie/src/GLVisualize/visualize/surface.jl index b91a77c1a1d..32046122ae2 100644 --- a/GLMakie/src/GLVisualize/visualize/surface.jl +++ b/GLMakie/src/GLVisualize/visualize/surface.jl @@ -104,6 +104,7 @@ function surface(main, s::Style{:surface}, data::Dict) uv_scale = Vec2f(1) instances = const_lift(x->(size(x,1)-1) * (size(x,2)-1), main) => "number of planes used to render the surface" + transparency = false shader = GLVisualizeShader( "fragment_output.frag", "util.vert", "surface.vert", "standard.frag", @@ -111,6 +112,8 @@ function surface(main, s::Style{:surface}, data::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), + "buffers" => output_buffers(to_value(transparency)), + "buffer_writes" => output_buffer_writes(to_value(transparency)) ) ) end diff --git a/GLMakie/src/GLVisualize/visualize_interface.jl b/GLMakie/src/GLVisualize/visualize_interface.jl index 8ab7f7c6a9e..5cabe63be1a 100644 --- a/GLMakie/src/GLVisualize/visualize_interface.jl +++ b/GLMakie/src/GLVisualize/visualize_interface.jl @@ -74,8 +74,6 @@ struct GLVisualizeShader <: AbstractLazyShader 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")] @@ -170,10 +168,15 @@ end # Make changes to fragment_output to match what's needed for postprocessing using ..GLMakie: enable_SSAO -function get_buffers() - if enable_SSAO[] + +function output_buffers(transparency = false) + if transparency + """ + layout(location=2) out float coverage; + """ + elseif enable_SSAO[] """ - layout(location=2) out vec4 fragment_position; + layout(location=2) out vec3 fragment_position; layout(location=3) out vec3 fragment_normal_occlusion; """ else @@ -181,13 +184,21 @@ function get_buffers() end end -function get_buffer_writes() - if enable_SSAO[] +function output_buffer_writes(transparency = false) + if transparency """ + float weight = color.a * max(0.01, 3000 * pow((1 - gl_FragCoord.z), 3)); + coverage = color.a; + fragment_color.rgb = weight * color.rgb; + fragment_color.a = weight; + """ + elseif enable_SSAO[] + """ + fragment_color = color; fragment_position = o_view_pos; fragment_normal_occlusion.xyz = o_normal; """ else - "" + "fragment_color = color;" end end diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 13402859b2f..487d790ce06 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -11,64 +11,114 @@ end mutable struct GLFramebuffer resolution::Observable{NTuple{2, Int}} - id::NTuple{2, GLuint} + id::GLuint + buffer_ids::Dict{Symbol, 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]) +Base.haskey(fb::GLFramebuffer, key::Symbol) = haskey(fb.buffers, key) +Base.getindex(fb::GLFramebuffer, key::Symbol) = fb.buffer_ids[key] => fb.buffers[key] + +function getfallback(fb::GLFramebuffer, key::Symbol, fallback_key::Symbol) + haskey(fb, key) ? fb[key] : fb[fallback_key] +end 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) +# attach texture as color attachment with automatic id picking +function attach_colorbuffer!(fb::GLFramebuffer, key::Symbol, t::Texture{T, 2}) where T + if haskey(fb.buffer_ids, key) || haskey(fb.buffers, key) + error("Key $key already exists.") + end + + max_color_id = GL_COLOR_ATTACHMENT0 + for id in values(fb.buffer_ids) + if GL_COLOR_ATTACHMENT0 <= id <= GL_COLOR_ATTACHMENT15 && id > max_color_id + max_color_id = id + end + end + next_color_id = max_color_id + 0x1 + if next_color_id > GL_COLOR_ATTACHMENT15 + error("Ran out of color buffers.") + end - 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) + glFramebufferTexture2D(GL_FRAMEBUFFER, next_color_id, GL_TEXTURE_2D, t.id, 0) + push!(fb.buffer_ids, key => next_color_id) + push!(fb.buffers, key => t) + return next_color_id +end +function GLFramebuffer(fb_size::NTuple{2, Int}) + # Create framebuffer + frambuffer_id = glGenFramebuffers() + glBindFramebuffer(GL_FRAMEBUFFER, frambuffer_id) + + # Buffers we always need + # Holds the image that eventually gets displayed + color_buffer = Texture( + RGBA{N0f8}, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge + ) + # Holds a (plot id, element id) for point picking + objectid_buffer = Texture( + Vec{2, GLuint}, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge + ) + # holds depth and stencil values 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 ) + # Order Independent Transparency + HDR_color_buffer = Texture( + RGBA{Float16}, fb_size, minfilter = :linear, x_repeat = :clamp_to_edge + ) + OIT_weight_buffer = Texture( + N0f8, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge + ) attach_framebuffer(color_buffer, GL_COLOR_ATTACHMENT0) attach_framebuffer(objectid_buffer, GL_COLOR_ATTACHMENT1) + attach_framebuffer(HDR_color_buffer, GL_COLOR_ATTACHMENT2) + attach_framebuffer(OIT_weight_buffer, GL_COLOR_ATTACHMENT3) 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 = Observable(fb_size) + # To allow adding postprocessors in various combinations we need to keep + # track of the buffer ids that are already in use. We may also want to reuse + # buffers so we give them names for easy fetching. + buffer_ids = Dict( + :color => GL_COLOR_ATTACHMENT0, + :objectid => GL_COLOR_ATTACHMENT1, + :HDR_color => GL_COLOR_ATTACHMENT2, + :OIT_weight => GL_COLOR_ATTACHMENT3, + :depth => GL_DEPTH_ATTACHMENT, + :stencil => GL_STENCIL_ATTACHMENT, + ) buffers = Dict( - :color => color_buffer, + :color => color_buffer, :objectid => objectid_buffer, - :depth => depth_buffer + :HDR_color => HDR_color_buffer, + :OIT_weight => OIT_weight_buffer, + :depth => depth_buffer, + :stencil => depth_buffer ) return GLFramebuffer( - fb_size_node, - (render_framebuffer, color_luma_framebuffer), - buffers, + fb_size_node, frambuffer_id, + buffer_ids, buffers, [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1] ) end diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index a797819953d..b1d0d52a7b9 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -16,46 +16,88 @@ function (sp::PostprocessPrerender)() return end -const PostProcessROBJ = RenderObject{PostprocessPrerender} - rcpframe(x) = 1f0 ./ Vec2f(x[1], x[2]) struct PostProcessor{F} - robjs::Vector{PostProcessROBJ} + robjs::Vector{RenderObject} render::F end function empty_postprocessor(args...; kwargs...) - PostProcessor(PostProcessROBJ[], screen -> nothing) + PostProcessor(RenderObject[], screen -> nothing) end +function OIT_postprocessor(framebuffer) + # Based on https://jcgt.org/published/0002/02/09/, see #1390 + # OIT setup + shader = LazyShader( + loadshader("postprocessing/fullscreen.vert"), + loadshader("postprocessing/OIT_blend.frag") + ) + data = Dict{Symbol, Any}( + # :opaque_color => framebuffer[:color][2], + :sum_color => framebuffer[:HDR_color][2], + :prod_alpha => framebuffer[:OIT_weight][2], + ) + pass = RenderObject( + data, shader, + () -> begin + glDepthMask(GL_TRUE) + glDisable(GL_DEPTH_TEST) + glDisable(GL_CULL_FACE) + glEnable(GL_BLEND) + # shader computes: + # src.rgb = sum_color / sum_weight * (1 - prod_alpha) + # src.a = prod_alpha + # blending: (assumes opaque.a = 1) + # opaque.rgb = 1 * src.rgb + src.a * opaque.rgb + # opaque.a = 0 * src.a + 1 * opaque.a + glBlendFuncSeparate(GL_ONE, GL_SRC_ALPHA, GL_ZERO, GL_ONE) + end, + nothing + ) + pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) + + color_id = framebuffer[:color][1] + full_render = screen -> begin + fb = screen.framebuffer + w, h = size(fb) + + # Blend transparent onto opaque + glDrawBuffer(color_id) + glViewport(0, 0, w, h) + glDisable(GL_STENCIL_TEST) + GLAbstraction.render(pass) + end + + PostProcessor(RenderObject[pass], full_render) +end + + + function ssao_postprocessor(framebuffer) # Add missing buffers - if !haskey(framebuffer.buffers, :position) + if !haskey(framebuffer, :position) glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) position_buffer = Texture( - Vec4f, 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( - Vec4f, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge + Vec3f, 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) + pos_id = attach_colorbuffer!(framebuffer, :position, position_buffer) + push!(framebuffer.render_buffer_ids, pos_id) 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) + if !haskey(framebuffer, :normal) + if !haskey(framebuffer, :HDR_color) + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) + normal_occlusion_buffer = Texture( + Vec4{Float16}, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge + ) + normal_occ_id = attach_colorbuffer!(framebuffer, :normal_occlusion, normal_occlusion_buffer) + else + normal_occ_id = framebuffer[:HDR_color][1] + end + push!(framebuffer.render_buffer_ids, normal_occ_id) end # SSAO setup @@ -79,8 +121,8 @@ function ssao_postprocessor(framebuffer) ) ) data1 = Dict{Symbol, Any}( - :position_buffer => framebuffer.buffers[:position], - :normal_occlusion_buffer => framebuffer.buffers[:normal_occlusion], + :position_buffer => framebuffer[:position][2], + :normal_occlusion_buffer => getfallback(framebuffer, :normal_occlusion, :HDR_color)[2], :kernel => kernel, :noise => Texture( [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], @@ -101,9 +143,9 @@ function ssao_postprocessor(framebuffer) loadshader("postprocessing/SSAO_blur.frag") ) data2 = Dict{Symbol, Any}( - :normal_occlusion => framebuffer.buffers[:normal_occlusion], - :color_texture => framebuffer.buffers[:color], - :ids => framebuffer.buffers[:objectid], + :normal_occlusion => getfallback(framebuffer, :normal_occlusion, :HDR_color)[2], + :color_texture => framebuffer[:color][2], + :ids => framebuffer[:objectid][2], :inv_texel_size => lift(rcpframe, framebuffer.resolution), :blur_range => Observable(Int32(2)) ) @@ -111,17 +153,15 @@ function ssao_postprocessor(framebuffer) pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) - + color_id = framebuffer[:color][1] full_render = screen -> begin fb = screen.framebuffer w, h = size(fb) # Setup rendering # SSAO - calculate occlusion - glDrawBuffer(GL_COLOR_ATTACHMENT3) # occlusion buffer + glDrawBuffer(normal_occ_id) # 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) @@ -143,7 +183,7 @@ function ssao_postprocessor(framebuffer) # SSAO - blur occlusion and apply to color - glDrawBuffer(GL_COLOR_ATTACHMENT0) # color buffer + glDrawBuffer(color_id) # color buffer for (screenid, scene) in screen.screens # Select the area of one leaf scene isempty(scene.children) || continue @@ -157,7 +197,7 @@ function ssao_postprocessor(framebuffer) glDisable(GL_SCISSOR_TEST) end - PostProcessor([pass1, pass2], full_render) + PostProcessor(RenderObject[pass1, pass2], full_render) end @@ -169,13 +209,16 @@ 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) + if !haskey(framebuffer, :color_luma) + if !haskey(framebuffer, :HDR_color) + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) + color_luma_buffer = Texture( + RGBA{N0f8}, size(framebuffer), minfilter=:linear, x_repeat=:clamp_to_edge + ) + luma_id = attach_colorbuffer!(framebuffer, :color_luma, color_luma_buffer) + else + luma_id = framebuffer[:HDR_color][1] + end end # calculate luma for FXAA @@ -184,7 +227,7 @@ function fxaa_postprocessor(framebuffer) loadshader("postprocessing/postprocess.frag") ) data1 = Dict{Symbol, Any}( - :color_texture => framebuffer.buffers[:color] + :color_texture => framebuffer[:color][2] ) pass1 = RenderObject(data1, shader1, PostprocessPrerender(), nothing) pass1.postrenderfunction = () -> draw_fullscreen(pass1.vertexarray.id) @@ -195,19 +238,19 @@ function fxaa_postprocessor(framebuffer) loadshader("postprocessing/fxaa.frag") ) data2 = Dict{Symbol, Any}( - :color_texture => framebuffer.buffers[:color_luma], + :color_texture => getfallback(framebuffer, :color_luma, :HDR_color)[2], :RCPFrame => lift(rcpframe, framebuffer.resolution), ) pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing) pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) + color_id = framebuffer[:color][1] 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 + glDrawBuffer(luma_id) glViewport(0, 0, w, h) # necessary with negative SSAO bias... glClearColor(1, 1, 1, 1) @@ -215,13 +258,11 @@ function fxaa_postprocessor(framebuffer) 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 + glDrawBuffer(color_id) # color buffer GLAbstraction.render(pass2) end - PostProcessor([pass1, pass2], full_render) + PostProcessor(RenderObject[pass1, pass2], full_render) end @@ -238,7 +279,7 @@ function to_screen_postprocessor(framebuffer) loadshader("postprocessing/copy.frag") ) data = Dict{Symbol, Any}( - :color_texture => framebuffer.buffers[:color] + :color_texture => framebuffer[:color][2] ) pass = RenderObject(data, shader, PostprocessPrerender(), nothing) pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) @@ -254,5 +295,5 @@ function to_screen_postprocessor(framebuffer) GLAbstraction.render(pass) # copy postprocess end - PostProcessor([pass], full_render) + PostProcessor(RenderObject[pass], full_render) end diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index 13758193bd1..1bd741110de 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -119,6 +119,9 @@ const selection_queries = Function[] Renders a single frame of a `window` """ function render_frame(screen::Screen; resize_buffers=true) + # NOTE + # The transparent color buffer is reused by SSAO and FXAA. Changing the + # render order here may introduce artifacts because of that. nw = to_native(screen) ShaderAbstractions.is_context_active(nw) || return fb = screen.framebuffer @@ -130,7 +133,7 @@ function render_frame(screen::Screen; resize_buffers=true) # prepare stencil (for sub-scenes) glEnable(GL_STENCIL_TEST) - glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) # color framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, fb.id) glDrawBuffers(length(fb.render_buffer_ids), fb.render_buffer_ids) glEnable(GL_STENCIL_TEST) glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE) @@ -138,6 +141,7 @@ function render_frame(screen::Screen; resize_buffers=true) 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) @@ -146,8 +150,7 @@ function render_frame(screen::Screen; resize_buffers=true) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE) glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE) glStencilMask(0x00) - GLAbstraction.render(screen, true, true) - + GLAbstraction.render(screen, false, true, true) # SSAO screen.postprocessors[1].render(screen) @@ -157,23 +160,47 @@ function render_frame(screen::Screen; resize_buffers=true) glEnable(GL_STENCIL_TEST) glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE) glStencilMask(0x00) - GLAbstraction.render(screen, true, false) + GLAbstraction.render(screen, false, true, false) glDisable(GL_STENCIL_TEST) - # FXAA + + # TRANSPARENT RENDER + # clear sums to 0 + glDrawBuffer(GL_COLOR_ATTACHMENT2) + glClearColor(0, 0, 0, 0) + glClear(GL_COLOR_BUFFER_BIT) + # clear alpha product to 1 + glDrawBuffer(GL_COLOR_ATTACHMENT3) + glClearColor(1, 1, 1, 1) + glClear(GL_COLOR_BUFFER_BIT) + # draw + glDrawBuffers(3, [GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT3]) + glEnable(GL_STENCIL_TEST) + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE) + glStencilMask(0x00) + GLAbstraction.render(screen, true, true, true) + GLAbstraction.render(screen, true, true, false) + GLAbstraction.render(screen, true, false, true) + GLAbstraction.render(screen, true, false, false) + glDisable(GL_STENCIL_TEST) + + # TRANSPARENT BLEND screen.postprocessors[2].render(screen) + # FXAA + screen.postprocessors[3].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) + GLAbstraction.render(screen, false, false, true) + GLAbstraction.render(screen, false, false, false) glDisable(GL_STENCIL_TEST) # transfer everything to the screen - screen.postprocessors[3].render(screen) + screen.postprocessors[4].render(screen) return @@ -187,14 +214,20 @@ function id2scene(screen, id1) return false, nothing end -function GLAbstraction.render(screen::GLScreen, fxaa::Bool, ssao::Bool=false) +function GLAbstraction.render(screen::GLScreen, transparent::Bool, fxaa::Bool, ssao::Bool) # 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 + if !((elem[:transparency][] == transparent) && + (elem[:fxaa][] == fxaa) && (elem[:ssao][] == ssao)) + continue + end + found, scene = id2scene(screen, screenid) found || continue + a = pixelarea(scene)[] glViewport(minimum(a)..., widths(a)...) if scene.clear @@ -205,15 +238,8 @@ function GLAbstraction.render(screen::GLScreen, fxaa::Bool, ssao::Bool=false) # 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 + + render(elem) end catch e @error "Error while rendering!" exception = e diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 41ace5480b8..74fd98d03f9 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -359,6 +359,7 @@ function Screen(; postprocessors = [ enable_SSAO[] ? ssao_postprocessor(fb) : empty_postprocessor(), + OIT_postprocessor(fb), enable_FXAA[] ? fxaa_postprocessor(fb) : empty_postprocessor(), to_screen_postprocessor(fb) ] diff --git a/ReferenceTests/src/tests/examples3d.jl b/ReferenceTests/src/tests/examples3d.jl index 1cc44e4181f..77a1a179906 100644 --- a/ReferenceTests/src/tests/examples3d.jl +++ b/ReferenceTests/src/tests/examples3d.jl @@ -548,3 +548,31 @@ end fig end + + +@cell "Order Independent Transparency" begin + # top row (yellow, cyan, magenta) contains stacks with the same alpha value + # bottom row (red, green, blue) contains stacks with varying alpha values + fig = Figure() + ax = LScene(fig[1, 1]) + r = Rect2f(-1, -1, 2, 2) + for x in (0, 1) + for (i, a) in enumerate((0.25, 0.5, 0.75, 1.0)) + ps = [Point3f(a, (0.15 + 0.01y)*(2x-1) , 0.2y) for y in 1:8] + if x == 0 + cs = [RGBAf(1, 0, 0, 0.75), RGBAf(0, 1, 0, 0.5), RGBAf(0, 0, 1, 0.25)] + elseif x == 1 + cs = [RGBAf(1, x, 0, a), RGBAf(0, 1, x, a), RGBAf(x, 0, 1, a)] + end + idxs = [1, 2, 3, 2, 1, 3, 1, 2, 1, 2, 3][i:7+i] + meshscatter!( + ax, ps, marker = r, + color = cs[idxs], transparency = true + ) + end + end + cam = cameracontrols(ax.scene) + cam.attributes.fov[] = 22f0 + update_cam!(ax.scene, cam, Vec3f(0.625, 0, 3.5), Vec3f(0.625, 0, 0), Vec3f(0, 1, 0)) + fig +end \ No newline at end of file diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index db203c6cac0..22e58056e5b 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -35,6 +35,7 @@ excludes = Set([ "Stars", "heatmaps & surface", "OldAxis + Surface", + "Order Independent Transparency", "Record Video" ]) @@ -43,4 +44,4 @@ database = database_filtered(excludes, excludes2) recorded = joinpath(@__DIR__, "recorded") rm(recorded; force=true, recursive=true); mkdir(recorded) -ReferenceTests.run_reference_tests(database, recorded; difference=0.026) +ReferenceTests.run_reference_tests(database, recorded; difference=0.032) diff --git a/docs/documentation/transparency.md b/docs/documentation/transparency.md new file mode 100644 index 00000000000..0457a047eab --- /dev/null +++ b/docs/documentation/transparency.md @@ -0,0 +1,142 @@ +# Basic transparency + +To make a plot transparent you need to add an alpha value to its `color` or `colormap`. + +\begin{examplefigure}{} +```julia +using CairoMakie, FileIO +CairoMakie.activate!() # hide + +# color +fig, ax, p = image(0..11, -1..11, rotr90(FileIO.load(Makie.assetpath("cow.png")))) +scatter!(ax, 1:10,fill(10, 10), markersize = 30, color = :red) +scatter!(ax, 1:10, fill(9, 10), markersize = 30, color = (:red, 0.5)) +scatter!(ax, 1:10, fill(8, 10), markersize = 30, color = RGBf(0.8, 0.6, 0.1)) +scatter!(ax, 1:10, fill(7, 10), markersize = 30, color = RGBAf(0.8, 0.6, 0.1, 0.5)) + +# colormap +scatter!(ax, 1:10, fill(5, 10), markersize = 30, color = 1:10, colormap = :viridis) +scatter!(ax, 1:10, fill(4, 10), markersize = 30, color = 1:10, colormap = (:viridis, 0.5),) +scatter!(ax, 1:10, fill(3, 10), markersize = 30, color = 1:10, colormap = (:red, :orange),) +scatter!(ax, 1:10, fill(2, 10), markersize = 30, color = 1:10, colormap = ((:red, 0.5), (:orange, 0.5))) +cm = [RGBf(x^2, 1 - x^2, 0.2) for x in range(0, 1, length=100)] +scatter!(ax, 1:10, fill(1, 10), markersize = 30, color = 1:10, colormap = cm) +cm = [RGBAf(x^2, 1 - x^2, 0.2, 0.5) for x in range(0, 1, length=100)] +scatter!(ax, 1:10, fill(0, 10), markersize = 30, color = 1:10, colormap = cm) +fig +``` +\end{examplefigure} + + +# Details and Problems with transparency + +The color generated from two overlapping transparent objects depends on their order. Consider for example a red and blue marker with the same level of transparency. If the blue marker is in front we expect a more blue color where they overlap. If the red one is in front we expect a more red color. + +\begin{examplefigure}{} +```julia +using CairoMakie +CairoMakie.activate!() # hide + +scene = Scene(resolution = (400, 275)) +campixel!(scene) +scatter!( + scene, [100, 200, 300], [100, 100, 100], + color = [RGBAf(1,0,0,0.5), RGBAf(0,0,1,0.5), RGBAf(1,0,0,0.5)], + markersize=150 +) +scatter!(scene, Point2f(150, 175), color = (:green, 0.5), markersize=150) +scene +``` +\end{examplefigure} + +The graphic above follows two rules in terms of transparency: + +1. The drawing order of plots matches their creation order. I.e. the first plot is behind the second. +2. Plot elements are drawn in order. I.e. the first scattered marker is behind the second which is behind the third. + +This works fine if our concept of depth matches drawing order. But if we explicitly add depth to our plot this idea can fail. Take for example two planes rotated to have a varying depth value: + +\begin{examplefigure}{} +```julia +using CairoMakie +CairoMakie.activate!() # hide + +fig = Figure() +ax = LScene(fig[1, 1], show_axis=false) +p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = false) +p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = false) +rotate!(p1, Vec3f(0, 1, 0), 0.1) +rotate!(p2, Vec3f(0, 1, 0), -0.1) +fig +``` +\end{examplefigure} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +fig = Figure() +ax = LScene(fig[1, 1], show_axis=false) +p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = false) +p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = false) +rotate!(p1, Vec3f(0, 1, 0), 0.1) +rotate!(p2, Vec3f(0, 1, 0), -0.1) +fig +``` +\end{examplefigure} + +Both backends handle this wrong. CairoMakie seems to ignore depth and just draws the planes in plotting order. This isn't quite true - CairoMakie does consider depth on a per-plot basis and in some cases on a per-element basis (e.g. triangles in a 3D mesh). But it can't handle depth on a per pixel level. + +GLMakie on the other hand can handle depth on a per-pixel level, as evident by the correct order shown above. The problem with transparency here is that the order of colors applied to a pixel is not known a priori. GLMakie will draw the red plane first and record depth values for each pixel. Then it will draw the blue plane if it's in front of the other. Solving this exactly would require collecting colors and depth values per pixel, sorting them and then blending them in order. This would be very expensive and is therefore rarely done. + + +## Order independent transparency + +GLMakie implements an approximate scheme for blending transparent colors - [Order Independent Transparency](https://jcgt.org/published/0002/02/09/) (OIT). Instead of using the usual order dependent blending `alpha * color + (1 - alpha) * background_color` it uses a weighted sum with weights based on depth and alpha. You can turn on OIT by setting `transparency = true` for a given plot. + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +fig = Figure() +ax = LScene(fig[1, 1], show_axis=false) +p1 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = false, transparency = true) +p2 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:blue, 0.5), shading = false, transparency = true) +p3 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = false, transparency = true) +for (dz, p) in zip((-1, 0, 1), (p1, p2, p3)) + translate!(p, 0, 0, dz) +end +fig +``` +\end{examplefigure} + +Being an approximate scheme OIT has some strengths and weaknesses. There are two significant drawbacks of OIT: +1. Blending always happens - even if a fully opaque color (alpha = 1) should hide another. +2. Blending isn't sharp - when two colors with the same alpha value are blended at similar depth values their output color will be similar. + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +fig = Figure(resolution = (800, 400)) +ax1 = LScene(fig[1, 1], show_axis=false) +p1 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :red, shading = false, transparency = true) +p2 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :blue, shading = false, transparency = true) +p3 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :red, shading = false, transparency = true) +for (dz, p) in zip((-1, 0, 1), (p1, p2, p3)) + translate!(p, 0, 0, dz) +end + +ax2 = LScene(fig[1, 2], show_axis=false) +p1 = mesh!(ax2, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = false, transparency=true) +p2 = mesh!(ax2, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = false, transparency=true) +rotate!(p1, Vec3f(0, 1, 0), 0.1) +rotate!(p2, Vec3f(0, 1, 0), -0.1) +fig +``` +\end{examplefigure} + +Note that you can mix opaque `transparency = false` plots with transparent OIT plots without problems. So the first issue is not really an issue for truly opaque plots but rather close to opaque plots. \ No newline at end of file diff --git a/src/basic_recipes/band.jl b/src/basic_recipes/band.jl index 27d18427eb6..8d59f81bef6 100644 --- a/src/basic_recipes/band.jl +++ b/src/basic_recipes/band.jl @@ -9,10 +9,12 @@ between the points in `lower` and `upper`. $(ATTRIBUTES) """ @recipe(Band, lowerpoints, upperpoints) do scene - Attributes(; + attr = Attributes(; default_theme(scene, Mesh)..., colorrange = automatic, ) + attr[:shading][] = false + attr end convert_arguments(::Type{<: Band}, x, ylower, yupper) = (Point2f.(x, ylower), Point2f.(x, yupper)) @@ -47,32 +49,29 @@ function plot!(plot::Band) end end - mesh!(plot, coordinates, connectivity; - color = meshcolor, colormap = plot[:colormap], - colorrange = plot[:colorrange], - shading = false, visible = plot[:visible], - inspectable = plot[:inspectable] - ) + attr = Attributes(plot) + attr[:color] = meshcolor + mesh!(plot, attr, coordinates, connectivity) end function fill_view(x, y1, y2, where::Nothing) x, y1, y2 - end - function fill_view(x, y1, y2, where::Function) +end +function fill_view(x, y1, y2, where::Function) fill_view(x, y1, y2, where.(x, y1, y2)) - end - function fill_view(x, y1, y2, bools::AbstractVector{<: Union{Integer, Bool}}) +end +function fill_view(x, y1, y2, bools::AbstractVector{<: Union{Integer, Bool}}) view(x, bools), view(y1, bools), view(y2, bools) - end +end - """ - fill_between!(scenelike, x, y1, y2; where = nothing, kw_args...) +""" + fill_between!(scenelike, x, y1, y2; where = nothing, kw_args...) - fill the section between 2 lines with the condition `where` - """ - function fill_between!(scenelike, x, y1, y2; where = nothing, kw_args...) +fill the section between 2 lines with the condition `where` +""" +function fill_between!(scenelike, x, y1, y2; where = nothing, kw_args...) xv, ylow, yhigh = fill_view(x, y1, y2, where) band!(scenelike, xv, ylow, yhigh; kw_args...) - end +end - export fill_between! +export fill_between! diff --git a/src/basic_recipes/barplot.jl b/src/basic_recipes/barplot.jl index 53d3458bb12..ef16c641a5c 100644 --- a/src/basic_recipes/barplot.jl +++ b/src/basic_recipes/barplot.jl @@ -42,7 +42,8 @@ $(ATTRIBUTES) label_offset = 5, label_font = theme(scene, :font), label_size = 20, - label_formatter = bar_label_formatter + label_formatter = bar_label_formatter, + transparency = false ) end @@ -252,7 +253,7 @@ function Makie.plot!(p::BarPlot) poly!( p, bars, color = p.color, colormap = p.colormap, colorrange = p.colorrange, strokewidth = p.strokewidth, strokecolor = p.strokecolor, visible = p.visible, - inspectable = p.inspectable + inspectable = p.inspectable, transparency = p.transparency ) if !isnothing(p.bar_labels[]) text!(p, labels; align=label_aligns, offset=label_offsets, color=label_colors, font=p.label_font, textsize=p.label_size, rotation=p.label_rotation) diff --git a/src/basic_recipes/contourf.jl b/src/basic_recipes/contourf.jl index 345a704647a..a8870b0537b 100644 --- a/src/basic_recipes/contourf.jl +++ b/src/basic_recipes/contourf.jl @@ -25,7 +25,8 @@ $(ATTRIBUTES) colormap = theme(scene, :colormap), extendlow = nothing, extendhigh = nothing, - inspectable = theme(scene, :inspectable) + inspectable = theme(scene, :inspectable), + transparency = false ) end @@ -153,8 +154,9 @@ function Makie.plot!(c::Contourf{<:Tuple{<:AbstractVector{<:Real}, <:AbstractVec color = colors, strokewidth = 0, strokecolor = :transparent, - shading=false, - inspectable = c.inspectable + shading = false, + inspectable = c.inspectable, + transparency = c.transparency ) end diff --git a/src/basic_recipes/contours.jl b/src/basic_recipes/contours.jl index d9f30c8b4af..af53aaa3613 100644 --- a/src/basic_recipes/contours.jl +++ b/src/basic_recipes/contours.jl @@ -111,13 +111,12 @@ function plot!(plot::Contour{<: Tuple{X, Y, Z, Vol}}) where {X, Y, Z, Vol} RGBAf(Colors.color(c), line ? alpha : 0.0) end end - volume!( - plot, x, y, z, volume, colormap = cmap, colorrange = cliprange, algorithm = 7, - transparency = plot.transparency, overdraw = plot.overdraw, - ambient = plot.ambient, diffuse = plot.diffuse, lightposition = plot.lightposition, - shininess = plot.shininess, specular = plot.specular, inspectable = plot.inspectable, - enable_depth = plot.enable_depth - ) + attr = Attributes(plot) + attr[:colorrange] = cliprange + attr[:colormap] = cmap + attr[:algorithm] = 7 + pop!(attr, :levels) + volume!(plot, attr, x, y, z, volume) end function color_per_level(color, colormap, colorrange, alpha, levels) diff --git a/src/basic_recipes/error_and_rangebars.jl b/src/basic_recipes/error_and_rangebars.jl index 9a9bf17d790..25bce01967b 100644 --- a/src/basic_recipes/error_and_rangebars.jl +++ b/src/basic_recipes/error_and_rangebars.jl @@ -25,7 +25,8 @@ $(ATTRIBUTES) direction = :y, visible = theme(scene, :visible), colormap = theme(scene, :colormap), - inspectable = theme(scene, :inspectable) + inspectable = theme(scene, :inspectable), + transparency = false ) end @@ -51,7 +52,8 @@ $(ATTRIBUTES) direction = :y, visible = theme(scene, :visible), colormap = theme(scene, :colormap), - inspectable = theme(scene, :inspectable) + inspectable = theme(scene, :inspectable), + transparency = false ) end @@ -181,7 +183,7 @@ function _plot_bars!(plot, linesegpairs, is_in_y_direction) f_if(condition, f, arg) = condition ? f(arg) : arg - @extract plot (whiskerwidth, color, linewidth, visible, colormap, inspectable) + @extract plot (whiskerwidth, color, linewidth, visible, colormap, inspectable, transparency) scene = parent_scene(plot) @@ -221,11 +223,12 @@ function _plot_bars!(plot, linesegpairs, is_in_y_direction) linesegments!( plot, linesegpairs, color = color, linewidth = linewidth, visible = visible, - colormap = colormap, inspectable = inspectable + colormap = colormap, inspectable = inspectable, transparency = transparency ) linesegments!( plot, whiskers, color = whiskercolors, linewidth = whiskerlinewidths, - visible = visible, colormap = colormap, inspectable = inspectable + visible = visible, colormap = colormap, inspectable = inspectable, + transparency = transparency ) plot end diff --git a/src/basic_recipes/pie.jl b/src/basic_recipes/pie.jl index 0b6f2cd7605..206bf6ae633 100644 --- a/src/basic_recipes/pie.jl +++ b/src/basic_recipes/pie.jl @@ -17,7 +17,8 @@ $(ATTRIBUTES) inner_radius = 0, offset = 0, inspectable = theme(scene, :inspectable), - visible = true + visible = true, + transparency = false ) end @@ -64,8 +65,12 @@ function plot!(plot::Pie) end # plot pieces as polys - poly!(plot, polys, color = plot.color, strokewidth = plot.strokewidth, - strokecolor = plot.strokecolor, inspectable = plot.inspectable, visible = plot.visible) + poly!( + plot, polys, + color = plot.color, strokewidth = plot.strokewidth, + strokecolor = plot.strokecolor, inspectable = plot.inspectable, + visible = plot.visible, transparency = plot.transparency + ) plot end diff --git a/src/basic_recipes/poly.jl b/src/basic_recipes/poly.jl index 4a3af16dc7c..0e31a6c497c 100644 --- a/src/basic_recipes/poly.jl +++ b/src/basic_recipes/poly.jl @@ -54,13 +54,13 @@ function plot!(plot::Poly{<: Tuple{Union{GeometryBasics.Mesh, GeometryPrimitive} plot, plot[1], color = plot[:color], colormap = plot[:colormap], colorrange = plot[:colorrange], shading = plot[:shading], visible = plot[:visible], overdraw = plot[:overdraw], - inspectable = plot[:inspectable] + inspectable = plot[:inspectable], transparency = plot[:transparency] ) wireframe!( plot, plot[1], color = plot[:strokecolor], linestyle = plot[:linestyle], linewidth = plot[:strokewidth], visible = plot[:visible], overdraw = plot[:overdraw], - inspectable = plot[:inspectable] + inspectable = plot[:inspectable], transparency = plot[:transparency] ) end @@ -138,7 +138,8 @@ function plot!(plot::Mesh{<: Tuple{<: AbstractVector{P}}}) where P <: Union{Abst meshes = plot[1] color_node = plot.color attributes = Attributes( - visible = plot.visible, shading = plot.shading, fxaa=plot.fxaa, inspectable = plot.inspectable + visible = plot.visible, shading = plot.shading, fxaa = plot.fxaa, + inspectable = plot.inspectable, transparency = plot.transparency ) attributes[:colormap] = get(plot, :colormap, nothing) diff --git a/src/basic_recipes/streamplot.jl b/src/basic_recipes/streamplot.jl index 496df37e2ad..008dcfe6353 100644 --- a/src/basic_recipes/streamplot.jl +++ b/src/basic_recipes/streamplot.jl @@ -205,6 +205,6 @@ function plot!(p::StreamPlot) marker = @lift(arrow_head(N, $(p.arrow_head), $(p.quality))), color = lift(x-> x[4], data), rotations = rotations, colormap = p.colormap, colorrange = p.colorrange, - inspectable = p.inspectable + inspectable = p.inspectable, transparency = p.transparency ) end