diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index 13fb39e2727..3d920610c44 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -496,13 +496,14 @@ end function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Text{<:Tuple{<:Union{AbstractArray{<:Makie.GlyphCollection}, Makie.GlyphCollection}}})) ctx = screen.context @get_attribute(primitive, (rotation, model, space, markerspace, offset)) + transform_marker = to_value(get(primitive, :transform_marker, true))::Bool position = primitive.position[] # use cached glyph info glyph_collection = to_value(primitive[1]) draw_glyph_collection( scene, ctx, position, glyph_collection, remove_billboard(rotation), - model, space, markerspace, offset, primitive.transformation + model, space, markerspace, offset, primitive.transformation, transform_marker ) nothing @@ -511,21 +512,23 @@ end function draw_glyph_collection( scene, ctx, positions, glyph_collections::AbstractArray, rotation, - model::Mat, space, markerspace, offset, transformation + model::Mat, space, markerspace, offset, transformation, transform_marker ) # TODO: why is the Ref around model necessary? doesn't broadcast_foreach handle staticarrays matrices? broadcast_foreach(positions, glyph_collections, rotation, Ref(model), space, markerspace, offset) do pos, glayout, ro, mo, sp, msp, off - draw_glyph_collection(scene, ctx, pos, glayout, ro, mo, sp, msp, off, transformation) + draw_glyph_collection(scene, ctx, pos, glayout, ro, mo, sp, msp, off, transformation, transform_marker) end end _deref(x) = x _deref(x::Ref) = x[] -function draw_glyph_collection(scene, ctx, position, glyph_collection, rotation, _model, space, markerspace, offsets, transformation) +function draw_glyph_collection( + scene, ctx, position, glyph_collection, rotation, _model, space, + markerspace, offsets, transformation, transform_marker) glyphs = glyph_collection.glyphs glyphoffsets = glyph_collection.origins @@ -537,7 +540,7 @@ function draw_glyph_collection(scene, ctx, position, glyph_collection, rotation, strokecolors = glyph_collection.strokecolors model = _deref(_model) - model33 = model[Vec(1, 2, 3), Vec(1, 2, 3)] + model33 = transform_marker ? model[Vec(1, 2, 3), Vec(1, 2, 3)] : Mat3f(I) id = Mat4f(I) glyph_pos = let @@ -859,7 +862,7 @@ end nan2zero(x) = !isnan(x) * x -function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f0) +function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f0, rotation = Mat4f(I)) @get_attribute(attributes, (shading, diffuse, specular, shininess, faceculling)) matcap = to_value(get(attributes, :matcap, nothing)) @@ -876,44 +879,61 @@ function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f model = attributes.model[]::Mat4f space = to_value(get(attributes, :space, :data))::Symbol func = Makie.transform_func(attributes) + + # TODO: assume Symbol here after this has been deprecated for a while + if shading isa Bool + @warn "`shading::Bool` is deprecated. Use `shading = NoShading` instead of false and `shading = FastShading` or `shading = MultiLightShading` instead of true." + shading_bool = shading + else + shading_bool = shading != NoShading + end + draw_mesh3D( - scene, screen, space, func, meshpoints, meshfaces, meshnormals, per_face_col, pos, scale, - model, shading::Bool, diffuse::Vec3f, + scene, screen, space, func, meshpoints, meshfaces, meshnormals, per_face_col, + pos, scale, rotation, + model, shading_bool::Bool, diffuse::Vec3f, specular::Vec3f, shininess::Float32, faceculling::Int ) end function draw_mesh3D( - scene, screen, space, transform_func, meshpoints, meshfaces, meshnormals, per_face_col, pos, scale, + scene, screen, space, transform_func, meshpoints, meshfaces, meshnormals, per_face_col, + pos, scale, rotation, model, shading, diffuse, specular, shininess, faceculling ) ctx = screen.context - view = ifelse(is_data_space(space), scene.camera.view[], Mat4f(I)) - projection = Makie.space_to_clip(scene.camera, space, false) + projectionview = Makie.space_to_clip(scene.camera, space, true) + eyeposition = scene.camera.eyeposition[] i = Vec(1, 2, 3) - normalmatrix = transpose(inv(view[i, i] * model[i, i])) - - # Mesh data - # transform to view/camera space + normalmatrix = transpose(inv(model[i, i])) + local_model = rotation * Makie.scalematrix(Vec3f(scale)) # pass transform_func as argument to function, so that we get a function barrier # and have `transform_func` be fully typed inside closure vs = broadcast(meshpoints, (transform_func,)) do v, f # Should v get a nan2zero? v = Makie.apply_transform(f, v, space) - p4d = to_ndim(Vec4f, scale .* to_ndim(Vec3f, v, 0f0), 1f0) - view * (model * p4d .+ to_ndim(Vec4f, pos, 0f0)) + p4d = to_ndim(Vec4f, to_ndim(Vec3f, v, 0f0), 1f0) + model * (local_model * p4d .+ to_ndim(Vec4f, pos, 0f0)) end ns = map(n -> normalize(normalmatrix * n), meshnormals) # Light math happens in view/camera space - pointlight = Makie.get_point_light(scene) - lightposition = if !isnothing(pointlight) - pointlight.position[] + dirlight = Makie.get_directional_light(scene) + if !isnothing(dirlight) + lightdirection = if dirlight.camera_relative + T = inv(scene.camera.view[][Vec(1,2,3), Vec(1,2,3)]) + normalize(T * dirlight.direction[]) + else + normalize(dirlight.direction[]) + end + c = dirlight.color[] + light_color = Vec3f(red(c), green(c), blue(c)) else - Vec3f(0) + lightdirection = Vec3f(0,0,-1) + light_color = Vec3f(0) end ambientlight = Makie.get_ambient_light(scene) @@ -924,11 +944,9 @@ function draw_mesh3D( Vec3f(0) end - lightpos = (view * to_ndim(Vec4f, lightposition, 1.0))[Vec(1, 2, 3)] - # Camera to screen space ts = map(vs) do v - clip = projection * v + clip = projectionview * v @inbounds begin p = (clip ./ clip[4])[Vec(1, 2)] p_yflip = Vec2f(p[1], -p[2]) @@ -938,6 +956,9 @@ function draw_mesh3D( return Vec3f(p[1], p[2], clip[3]) end + # vs are used as camdir (camera to vertex) for light calculation (in world space) + vs = map(v -> normalize(v[i] - eyeposition), vs) + # Approximate zorder average_zs = map(f -> average_z(ts, f), meshfaces) zorder = sortperm(average_zs) @@ -945,23 +966,23 @@ function draw_mesh3D( # Face culling zorder = filter(i -> any(last.(ns[meshfaces[i]]) .> faceculling), zorder) - draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightpos, shininess, diffuse, ambient, specular) + draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightdirection, light_color, shininess, diffuse, ambient, specular) return end -function _calculate_shaded_vertexcolors(N, v, c, lightpos, ambient, diffuse, specular, shininess) - L = normalize(lightpos .- v[Vec(1,2,3)]) - diff_coeff = max(dot(L, N), 0f0) - H = normalize(L + normalize(-v[Vec(1, 2, 3)])) - spec_coeff = max(dot(H, N), 0f0)^shininess +function _calculate_shaded_vertexcolors(N, v, c, lightdir, light_color, ambient, diffuse, specular, shininess) + L = lightdir + diff_coeff = max(dot(L, -N), 0f0) + H = normalize(L + v) + spec_coeff = max(dot(H, -N), 0f0)^shininess c = RGBAf(c) # if this is one expression it introduces allocations?? - new_c_part1 = (ambient .+ diff_coeff .* diffuse) .* Vec3f(c.r, c.g, c.b) #.+ - new_c = new_c_part1 .+ specular * spec_coeff + new_c_part1 = (ambient .+ light_color .* diff_coeff .* diffuse) .* Vec3f(c.r, c.g, c.b) #.+ + new_c = new_c_part1 .+ light_color .* specular * spec_coeff RGBAf(new_c..., c.alpha) end -function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightpos, shininess, diffuse, ambient, specular) +function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightdir, light_color, shininess, diffuse, ambient, specular) for k in reverse(zorder) f = meshfaces[k] @@ -969,7 +990,7 @@ function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, t1 = ts[f[1]] t2 = ts[f[2]] t3 = ts[f[3]] - + # skip any mesh segments with NaN points. if isnan(t1) || isnan(t2) || isnan(t3) continue @@ -984,7 +1005,7 @@ function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, N = ns[f[i]] v = vs[f[i]] c = facecolors[i] - _calculate_shaded_vertexcolors(N, v, c, lightpos, ambient, diffuse, specular, shininess) + _calculate_shaded_vertexcolors(N, v, c, lightdir, light_color, ambient, diffuse, specular, shininess) end else c1, c2, c3 = facecolors @@ -995,7 +1016,7 @@ function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, # c1 = RGB(n1...) # c2 = RGB(n2...) # c3 = RGB(n3...) - + pattern = Cairo.CairoPatternMesh() Cairo.mesh_pattern_begin_patch(pattern) @@ -1067,25 +1088,24 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki ) - if !(rotations isa Vector) - R = Makie.rotationmatrix4(to_rotation(rotations)) - submesh[:model] = model * R - end + submesh[:model] = model scales = primitive[:markersize][] for i in zorder p = pos[i] if color isa AbstractVector submesh[:calculated_colors] = color[i] end - if rotations isa Vector - R = Makie.rotationmatrix4(to_rotation(rotations[i])) - submesh[:model] = model * R - end scale = markersize isa Vector ? markersize[i] : markersize + rotation = if rotations isa Vector + Makie.rotationmatrix4(to_rotation(rotations[i])) + else + Makie.rotationmatrix4(to_rotation(rotations)) + end draw_mesh3D( scene, screen, submesh, marker, pos = p, - scale = scale isa Real ? Vec3f(scale) : to_ndim(Vec3f, scale, 1f0) + scale = scale isa Real ? Vec3f(scale) : to_ndim(Vec3f, scale, 1f0), + rotation = rotation ) end diff --git a/GLMakie/assets/shader/fragment_output.frag b/GLMakie/assets/shader/fragment_output.frag index 7fb14e78a9a..837d5ccc3ec 100644 --- a/GLMakie/assets/shader/fragment_output.frag +++ b/GLMakie/assets/shader/fragment_output.frag @@ -13,7 +13,7 @@ layout(location=1) out uvec2 fragment_groupid; in vec3 o_view_pos; -in vec3 o_normal; +in vec3 o_view_normal; void write2framebuffer(vec4 color, uvec2 id){ if(color.a <= 0.0) @@ -34,7 +34,7 @@ void write2framebuffer(vec4 color, uvec2 id){ // // if transparency == false && ssao = true // fragment_color = color; // fragment_position = o_view_pos; - // fragment_normal_occlusion.xyz = o_normal; + // fragment_normal_occlusion.xyz = o_view_normal; // // else // fragment_color = color; diff --git a/GLMakie/assets/shader/heatmap.vert b/GLMakie/assets/shader/heatmap.vert index df15cab1327..380bf802328 100644 --- a/GLMakie/assets/shader/heatmap.vert +++ b/GLMakie/assets/shader/heatmap.vert @@ -13,7 +13,7 @@ out vec2 o_uv; flat out uvec2 o_objectid; out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; ivec2 ind2sub(ivec2 dim, int linearindex){ return ivec2(linearindex % dim.x, linearindex / dim.x); @@ -22,7 +22,7 @@ ivec2 ind2sub(ivec2 dim, int linearindex){ void main(){ //Outputs for ssao, which we don't use for 2d shaders like heatmap/image o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); int index = gl_InstanceID; vec2 offset = vertices; diff --git a/GLMakie/assets/shader/lighting.frag b/GLMakie/assets/shader/lighting.frag new file mode 100644 index 00000000000..ecd6fd39364 --- /dev/null +++ b/GLMakie/assets/shader/lighting.frag @@ -0,0 +1,181 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +// Sets which shading procedures to use +// Options: +// NO_SHADING - skip shading calculation, handled outside +// FAST_SHADING - single point light (forward rendering) +// MULTI_LIGHT_SHADING - simple shading with multiple lights (forward rendering) +{{shading}} + + +// Shared uniforms, inputs and functions +#if defined FAST_SHADING || defined MULTI_LIGHT_SHADING + +// Generic uniforms +uniform vec3 diffuse; +uniform vec3 specular; +uniform float shininess; + +uniform float backlight; + +in vec3 o_camdir; +in vec3 o_world_pos; + +float smooth_zero_max(float x) { + // This is a smoothed version of max(value, 0.0) where -1 <= value <= 1 + // This comes from: + // c = 2 ^ -a # normalizes power w/o swaps + // xswap = (1 / c / a)^(1 / (a - 1)) - 1 # xval with derivative 1 + // yswap = c * (xswap+1) ^ a # yval with derivative 1 + // ifelse.(xs .< yswap, c .* (xs .+ 1 .+ xswap .- yswap) .^ a, xs) + // a = 16 constants: (harder edge) + // const float c = 0.0000152587890625, xswap = 0.7411011265922482, yswap = 0.10881882041201549; + // a = 8 constants: (softer edge) + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + return x < yswap ? c * pow(x + shift, 8) : x; +} + +vec3 blinn_phong(vec3 light_color, vec3 light_dir, vec3 camdir, vec3 normal, vec3 color) { + // diffuse coefficient (how directly does light hits the surface) + float diff_coeff = smooth_zero_max(dot(light_dir, -normal)) + + backlight * smooth_zero_max(dot(light_dir, normal)); + + // DEBUG - visualize diff_coeff, i.e. the angle between light and normals + // if (diff_coeff > 0.999) + // return vec3(0, 0, 1); + // else + // return vec3(1 - diff_coeff,diff_coeff, 0.05); + + // specular coefficient (does reflected light bounce into camera?) + vec3 H = normalize(light_dir + camdir); + float spec_coeff = pow(max(dot(H, -normal), 0.0), shininess) + + backlight * pow(max(dot(H, normal), 0.0), shininess); + if (diff_coeff <= 0.0 || isnan(spec_coeff)) + spec_coeff = 0.0; + + return light_color * vec3(diffuse * diff_coeff * color + specular * spec_coeff); +} + +#else // glsl fails to compile if the shader is just empty + +vec3 illuminate(vec3 normal, vec3 base_color); + +#endif + + +//////////////////////////////////////////////////////////////////////////////// +// FAST_SHADING // +//////////////////////////////////////////////////////////////////////////////// + + +#ifdef FAST_SHADING + +uniform vec3 ambient; +uniform vec3 light_color; +uniform vec3 light_direction; + +vec3 illuminate(vec3 world_pos, vec3 camdir, vec3 normal, vec3 base_color) { + vec3 shaded_color = blinn_phong(light_color, light_direction, camdir, normal, base_color); + return ambient * base_color + shaded_color; +} + +vec3 illuminate(vec3 normal, vec3 base_color) { + return illuminate(o_world_pos, normalize(o_camdir), normal, base_color); +} + +#endif + + +//////////////////////////////////////////////////////////////////////////////// +// MULTI_LIGHT_SHADING // +//////////////////////////////////////////////////////////////////////////////// + + +#ifdef MULTI_LIGHT_SHADING + +{{MAX_LIGHTS}} +{{MAX_LIGHT_PARAMETERS}} + +// differentiating different light sources +const int UNDEFINED = 0; +const int Ambient = 1; +const int PointLight = 2; +const int DirectionalLight = 3; +const int SpotLight = 4; + +// light parameters (maybe invalid depending on light type) +uniform int N_lights; +uniform int light_types[MAX_LIGHTS]; +uniform vec3 light_colors[MAX_LIGHTS]; +uniform float light_parameters[MAX_LIGHT_PARAMETERS]; + +vec3 calc_point_light(vec3 light_color, int idx, vec3 world_pos, vec3 camdir, vec3 normal, vec3 color) { + // extract args + vec3 position = vec3(light_parameters[idx], light_parameters[idx+1], light_parameters[idx+2]); + vec2 param = vec2(light_parameters[idx+3], light_parameters[idx+4]); + + // calculate light direction and distance + vec3 light_vec = world_pos - position; + + float dist = length(light_vec); + vec3 light_dir = normalize(light_vec); + + // How weak has the light gotten due to distance + // float attentuation = 1.0 / (1.0 + dist * dist * dist); + float attentuation = 1.0 / (1.0 + param.x * dist + param.y * dist * dist); + + return attentuation * blinn_phong(light_color, light_dir, camdir, normal, color); +} + +vec3 calc_directional_light(vec3 light_color, int idx, vec3 camdir, vec3 normal, vec3 color) { + vec3 light_dir = vec3(light_parameters[idx], light_parameters[idx+1], light_parameters[idx+2]); + return blinn_phong(light_color, light_dir, camdir, normal, color); +} + +vec3 calc_spot_light(vec3 light_color, int idx, vec3 world_pos, vec3 camdir, vec3 normal, vec3 color) { + // extract args + vec3 position = vec3(light_parameters[idx], light_parameters[idx+1], light_parameters[idx+2]); + vec3 spot_light_dir = normalize(vec3(light_parameters[idx+3], light_parameters[idx+4], light_parameters[idx+5])); + float inner_angle = light_parameters[idx+6]; // cos applied + float outer_angle = light_parameters[idx+7]; // cos applied + + vec3 light_dir = normalize(world_pos - position); + float intensity = smoothstep(outer_angle, inner_angle, dot(light_dir, spot_light_dir)); + + return intensity * blinn_phong(light_color, light_dir, camdir, normal, color); +} + +vec3 illuminate(vec3 world_pos, vec3 camdir, vec3 normal, vec3 base_color) { + vec3 final_color = vec3(0); + int idx = 0; + for (int i = 0; i < min(N_lights, MAX_LIGHTS); i++) { + switch (light_types[i]) { + case Ambient: + final_color += light_colors[i] * base_color; + break; + case PointLight: + final_color += calc_point_light(light_colors[i], idx, world_pos, camdir, normal, base_color); + idx += 5; // 3 position, 2 attenuation params + break; + case DirectionalLight: + final_color += calc_directional_light(light_colors[i], idx, camdir, normal, base_color); + idx += 3; // 3 direction + break; + case SpotLight: + final_color += calc_spot_light(light_colors[i], idx, world_pos, camdir, normal, base_color); + idx += 8; // 3 position, 3 direction, 1 parameter + break; + default: + return vec3(1,0,1); // debug magenta + } + } + return final_color; +} + +vec3 illuminate(vec3 normal, vec3 base_color) { + return illuminate(o_world_pos, normalize(o_camdir), normal, base_color); +} + +#endif diff --git a/GLMakie/assets/shader/line_segment.geom b/GLMakie/assets/shader/line_segment.geom index 2c0f8d473ef..ad8b9399b13 100644 --- a/GLMakie/assets/shader/line_segment.geom +++ b/GLMakie/assets/shader/line_segment.geom @@ -47,12 +47,12 @@ void emit_vertex(vec3 position, vec2 uv, int index) } out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; void main(void) { o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); // get the four vertices passed to the shader: vec3 p0 = screen_space(gl_in[0].gl_Position); // start of previous segment @@ -72,8 +72,8 @@ void main(void) vec3 AA_offset = AA_THICKNESS * v0; float AA = AA_THICKNESS * px2u; - /* 0 v0 l - | --> | + /* 0 v0 l + | --> | -thickness_aa0 - .----------------------------------. - -thickness_aa1 -g_thickness[0] - | .------------------------------. | - -g_thickness[1] | | | | @@ -95,8 +95,8 @@ void main(void) emit_vertex(p1 + thickness_aa1 * n0 + AA_offset, vec2(2*u + AA, -thickness_aa1), 1); emit_vertex(p1 - thickness_aa1 * n0 + AA_offset, vec2(2*u + AA, thickness_aa1), 1); #else - // For patterned lines AA is mostly done by the pattern sampling. We - // still set f_uv_minmax here to ensure that cut off patterns als have + // For patterned lines AA is mostly done by the pattern sampling. We + // still set f_uv_minmax here to ensure that cut off patterns als have // anti-aliasing at the start/end of this segment f_uv_minmax = vec2(0, u); emit_vertex(p0 + thickness_aa0 * n0 - AA_offset, vec2( - AA, -thickness_aa0), 0); diff --git a/GLMakie/assets/shader/lines.geom b/GLMakie/assets/shader/lines.geom index 833d435ebcc..f5683063430 100644 --- a/GLMakie/assets/shader/lines.geom +++ b/GLMakie/assets/shader/lines.geom @@ -24,7 +24,7 @@ flat out uvec2 f_id; flat out vec2 f_uv_minmax; out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; uniform vec2 resolution; uniform float pattern_length; @@ -773,7 +773,7 @@ void main(void) { // These need to be set but don't have reasonable values here o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); // we generate very thin lines for linewidth 0, so we manually skip them: if (g_thickness[1] == 0.0 && g_thickness[2] == 0.0) { diff --git a/GLMakie/assets/shader/mesh.frag b/GLMakie/assets/shader/mesh.frag index 26df220316d..5480da20008 100644 --- a/GLMakie/assets/shader/mesh.frag +++ b/GLMakie/assets/shader/mesh.frag @@ -4,15 +4,11 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d bool _; //empty structs are not allowed }; -uniform vec3 ambient; -uniform vec3 diffuse; -uniform vec3 specular; -uniform float shininess; -uniform float backlight; +// Sets which shading procedures to use +{{shading}} -in vec3 o_normal; -in vec3 o_lightdir; -in vec3 o_camdir; +in vec3 o_world_normal; +in vec3 o_view_normal; in vec4 o_color; in vec2 o_uv; flat in uvec2 o_id; @@ -68,7 +64,8 @@ vec4 get_color(sampler2D intensity, vec2 uv, vec2 color_norm, sampler1D color_ma return get_color_from_cmap(i, color_map, color_norm); } vec4 matcap_color(sampler2D matcap){ - vec2 muv = o_normal.xy * 0.5 + vec2(0.5, 0.5); + // TODO should matcaps use view space normals? + vec2 muv = o_view_normal.xy * 0.5 + vec2(0.5, 0.5); return texture(matcap, vec2(1.0-muv.y, muv.x)); } vec4 get_color(Nothing image, vec2 uv, Nothing color_norm, Nothing color_map, sampler2D matcap){ @@ -100,25 +97,11 @@ vec4 get_pattern_color(sampler2D color){ // Needs to exist for opengl to be happy vec4 get_pattern_color(Nothing color){return vec4(1,0,1,1);} -vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0); - - // specular coefficient - vec3 H = normalize(L + V); - - float spec_coeff = pow(max(dot(H, N), 0.0), shininess); - if (diff_coeff <= 0.0 || isnan(spec_coeff)) - spec_coeff = 0.0; - - // final lighting model - return vec3( - diffuse * diff_coeff * color + - specular * spec_coeff - ); -} - void write2framebuffer(vec4 color, uvec2 id); +#ifndef NO_SHADING +vec3 illuminate(vec3 normal, vec3 base_color); +#endif void main(){ vec4 color; @@ -128,6 +111,8 @@ void main(){ }else{ color = get_color(image, o_uv, color_norm, color_map, matcap); } - {{light_calc}} + #ifndef NO_SHADING + color.rgb = illuminate(normalize(o_world_normal), color.rgb); + #endif write2framebuffer(color, o_id); } diff --git a/GLMakie/assets/shader/mesh.vert b/GLMakie/assets/shader/mesh.vert index 75d3f0d8b3a..018248c0c5c 100644 --- a/GLMakie/assets/shader/mesh.vert +++ b/GLMakie/assets/shader/mesh.vert @@ -14,10 +14,9 @@ uniform bool interpolate_in_fragment_shader = false; in vec3 normals; -uniform vec3 lightposition; uniform mat4 projection, view, model; -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition); +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); vec4 get_color_from_cmap(float value, sampler1D color_map, vec2 colorrange); uniform uint objectid; @@ -63,5 +62,5 @@ void main() o_uv = vec2(1.0 - tex_uv.y, tex_uv.x) * uv_scale; o_color = to_color(vertex_color, color_map, color_norm); vec3 v = to_3d(vertices); - render(model * vec4(v, 1), normals, view, projection, lightposition); + render(model * vec4(v, 1), normals, view, projection); } diff --git a/GLMakie/assets/shader/particles.vert b/GLMakie/assets/shader/particles.vert index 8676a2a682f..2d26785e54d 100644 --- a/GLMakie/assets/shader/particles.vert +++ b/GLMakie/assets/shader/particles.vert @@ -28,7 +28,6 @@ in vec3 vertices; in vec3 normals; {{texturecoordinates_type}} texturecoordinates; -uniform vec3 lightposition; uniform mat4 view, model, projection; uniform uint objectid; uniform int len; @@ -91,7 +90,7 @@ vec4 get_particle_color(sampler2D color, Nothing intensity, Nothing color_map, N return vec4(0); } -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition); +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); vec2 get_uv(Nothing x){return vec2(0.0);} vec2 get_uv(vec2 x){return vec2(1.0 - x.y, x.x);} @@ -108,5 +107,5 @@ void main(){ o_color = o_color * to_color(vertex_color); o_uv = get_uv(texturecoordinates); rotate(rotation, index, V, N); - render(model * vec4(pos + V, 1), N, view, projection, lightposition); + render(model * vec4(pos + V, 1), N, view, projection); } diff --git a/GLMakie/assets/shader/sprites.geom b/GLMakie/assets/shader/sprites.geom index 106314219ba..fc771ef63b5 100644 --- a/GLMakie/assets/shader/sprites.geom +++ b/GLMakie/assets/shader/sprites.geom @@ -98,12 +98,12 @@ mat2 diagm(vec2 v){ } out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; void main(void) { o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); // emit quad as triangle strip // v3. ____ . v4 diff --git a/GLMakie/assets/shader/sprites.vert b/GLMakie/assets/shader/sprites.vert index 2489dddd040..ceee24efe7f 100644 --- a/GLMakie/assets/shader/sprites.vert +++ b/GLMakie/assets/shader/sprites.vert @@ -72,6 +72,7 @@ vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_ {{stroke_color_type}} stroke_color; {{glow_color_type}} glow_color; +uniform bool scale_primitive; uniform mat4 preprojection; uniform mat4 model; uniform uint objectid; @@ -96,7 +97,10 @@ void main(){ vec3 pos; {{position_calc}} vec4 p = preprojection * model * vec4(pos, 1); - g_position = p.xyz / p.w + mat3(model) * marker_offset; + if (scale_primitive) + g_position = p.xyz / p.w + mat3(model) * marker_offset; + else + g_position = p.xyz / p.w + marker_offset; g_offset_width.xy = quad_offset.xy; g_offset_width.zw = scale.xy; g_color = _color(color, intensity, color_map, color_norm, g_primitive_index, len); diff --git a/GLMakie/assets/shader/surface.vert b/GLMakie/assets/shader/surface.vert index e268131452a..b8b16eb98d6 100644 --- a/GLMakie/assets/shader/surface.vert +++ b/GLMakie/assets/shader/surface.vert @@ -19,8 +19,6 @@ in vec2 vertices; {{position_y_type}} position_y; uniform sampler2D position_z; -uniform vec3 lightposition; - {{image_type}} image; {{color_map_type}} color_map; {{color_norm_type}} color_norm; @@ -36,7 +34,7 @@ uniform vec3 scale; uniform mat4 view, model, projection; // See util.vert for implementations -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition); +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); ivec2 ind2sub(ivec2 dim, int linearindex); vec2 grid_pos(Grid2D pos, vec2 uv); vec2 linear_index(ivec2 dims, int index); @@ -172,6 +170,5 @@ void main() if (isnan(pos.z)) { pos.z = 0.0; } - - render(model * vec4(pos, 1), normalvec, view, projection, lightposition); + render(model * vec4(pos, 1), normalvec, view, projection); } diff --git a/GLMakie/assets/shader/util.vert b/GLMakie/assets/shader/util.vert index be3bdadfcc1..afb2c379945 100644 --- a/GLMakie/assets/shader/util.vert +++ b/GLMakie/assets/shader/util.vert @@ -1,5 +1,12 @@ {{GLSL_VERSION}} +// Sets which shading procedures to use +// Options: +// NO_SHADING - skip shading calculation, handled outside +// FAST_SHADING - single point light (forward rendering) +// MULTI_LIGHT_SHADING - simple shading with multiple lights (forward rendering) +{{shading}} + struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data bool _; //empty structs are not allowed }; @@ -241,33 +248,47 @@ vec4 _color(Nothing color, float intensity, sampler1D color_map, vec2 color_norm return get_color_from_cmap(intensity, color_map, color_norm); } -out vec3 o_view_pos; -out vec3 o_normal; -out vec3 o_lightdir; -out vec3 o_camdir; + +uniform float depth_shift; + +// TODO maybe ifdef SSAO this stuff? // transpose(inv(view * model)) // Transformation for vectors (rather than points) -uniform mat3 normalmatrix; -uniform vec3 lightposition; +uniform mat3 view_normalmatrix; +out vec3 o_view_pos; +out vec3 o_view_normal; + + +#if defined(FAST_SHADING) || defined(MULTI_LIGHT_SHADING) +// transpose(inv(model)) +uniform mat3 world_normalmatrix; uniform vec3 eyeposition; -uniform float depth_shift; +out vec3 o_world_pos; +out vec3 o_world_normal; +out vec3 o_camdir; +#endif -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition) +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection) { - // normal in world space - o_normal = normalmatrix * normal; // position in view space (as seen from camera) vec4 view_pos = view * position_world; + view_pos /= view_pos.w; + // position in clip space (w/ depth) gl_Position = projection * view_pos; gl_Position.z += gl_Position.w * depth_shift; - // direction to light - o_lightdir = normalize(view*vec4(lightposition, 1.0) - view_pos).xyz; - // direction to camera - // This is equivalent to - // normalize(view*vec4(eyeposition, 1.0) - view_pos).xyz - // (by definition `view * eyeposition = 0`) - o_camdir = normalize(-view_pos).xyz; + + // for lighting +#if defined(FAST_SHADING) || defined(MULTI_LIGHT_SHADING) + o_world_pos = position_world.xyz / position_world.w; + o_world_normal = world_normalmatrix * normal; + // direction from camera to vertex + o_camdir = position_world.xyz / position_world.w - eyeposition; +#endif + + // for SSAO o_view_pos = view_pos.xyz / view_pos.w; + // SSAO + matcap + o_view_normal = view_normalmatrix * normal; } diff --git a/GLMakie/assets/shader/volume.frag b/GLMakie/assets/shader/volume.frag index 4fdcadf3d25..c5a506229cd 100644 --- a/GLMakie/assets/shader/volume.frag +++ b/GLMakie/assets/shader/volume.frag @@ -1,10 +1,16 @@ {{GLSL_VERSION}} +// Sets which shading procedures to use +// Options: +// NO_SHADING - skip shading calculation, handled outside +// FAST_SHADING - single point light (forward rendering) +// MULTI_LIGHT_SHADING - simple shading with multiple lights (forward rendering) +{{shading}} + struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data bool _; //empty structs are not allowed }; in vec3 frag_vert; -in vec3 o_light_dir; {{volumedata_type}} volumedata; @@ -15,11 +21,6 @@ in vec3 o_light_dir; uniform float absorption = 1.0; uniform vec3 eyeposition; -uniform vec3 ambient; -uniform vec3 diffuse; -uniform vec3 specular; -uniform float shininess; - uniform mat4 modelinv; uniform int algorithm; uniform float isovalue; @@ -107,21 +108,9 @@ vec3 gennormal(vec3 uvw, float d) return normalize(a-b); } -// Includes front and back-facing normals (N, -N) -vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0) + max(dot(L, -N), 0.0); - // specular coefficient - vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0) + max(dot(H, -N), 0.0), shininess); - if (diff_coeff <= 0.0 || isnan(spec_coeff)) - spec_coeff = 0.0; - // final lighting model - return vec3( - ambient * color + - diffuse * diff_coeff * color + - specular * spec_coeff - ); -} +#ifndef NO_SHADING +vec3 illuminate(vec3 world_pos, vec3 camdir, vec3 normal, vec3 base_color); +#endif // Simple random generator found: http://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl float rand(){ @@ -208,7 +197,7 @@ vec4 contours(vec3 front, vec3 dir) float T = 1.0; vec3 Lo = vec3(0.0); int i = 0; - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); {{depth_init}} // may write: float depth = 100000.0; for (i; i < num_samples; ++i) { @@ -221,8 +210,8 @@ vec4 contours(vec3 front, vec3 dir) // vec4 frag_coord = projectionview * model * vec4(pos, 1); // depth = min(depth, frag_coord.z / frag_coord.w); vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); - vec3 opaque = blinnphong(N, camdir, L, density.rgb); + vec4 world_pos = model * vec4(pos, 1); + vec3 opaque = illuminate(world_pos.xyz / world_pos.w, camdir, N, density.rgb); Lo += (T * opacity) * opaque; T *= 1.0 - opacity; if (T <= 0.01) @@ -242,7 +231,7 @@ vec4 isosurface(vec3 front, vec3 dir) vec4 c = vec4(0.0); int i = 0; vec4 diffuse_color = color_lookup(isovalue, color_map, color_norm, color); - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); {{depth_init}} // may write: float depth = 100000.0; for (i; i < num_samples; ++i){ @@ -253,9 +242,9 @@ vec4 isosurface(vec3 front, vec3 dir) // vec4 frag_coord = projectionview * model * vec4(pos, 1); // depth = min(depth, frag_coord.z / frag_coord.w); vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); + vec4 world_pos = model * vec4(pos, 1); c = vec4( - blinnphong(N, camdir, L, diffuse_color.rgb), + illuminate(world_pos.xyz / world_pos.w, camdir, N, diffuse_color.rgb), diffuse_color.a ); break; diff --git a/GLMakie/assets/shader/volume.vert b/GLMakie/assets/shader/volume.vert index 72c60a336c8..2ca396b5763 100644 --- a/GLMakie/assets/shader/volume.vert +++ b/GLMakie/assets/shader/volume.vert @@ -3,24 +3,26 @@ in vec3 vertices; out vec3 frag_vert; -out vec3 o_light_dir; uniform mat4 projectionview, model; -uniform vec3 lightposition; uniform mat4 modelinv; uniform float depth_shift; +// SSAO out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; + +// Lighting (unused and don't need to be available?) +// out vec3 o_world_pos; +// out vec3 o_world_normal; void main() { // TODO set these in volume.frag o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); vec4 world_vert = model * vec4(vertices, 1); frag_vert = world_vert.xyz; - o_light_dir = vec3(modelinv * vec4(lightposition, 1)); gl_Position = projectionview * world_vert; gl_Position.z += gl_Position.w * depth_shift; } diff --git a/GLMakie/src/GLAbstraction/GLUniforms.jl b/GLMakie/src/GLAbstraction/GLUniforms.jl index ab6282c0ee4..a51af4024bc 100644 --- a/GLMakie/src/GLAbstraction/GLUniforms.jl +++ b/GLMakie/src/GLAbstraction/GLUniforms.jl @@ -241,6 +241,7 @@ gl_convert(x::Mat{N, M, T}) where {N, M, T} = map(gl_promote(T), x) gl_convert(a::AbstractVector{<: AbstractFace}) = indexbuffer(s) gl_convert(t::Type{T}, a::T; kw_args...) where T <: NATIVE_TYPES = a gl_convert(::Type{<: GPUArray}, a::StaticVector) = gl_convert(a) +gl_convert(x::Vector) = x function gl_convert(T::Type{<: GPUArray}, a::AbstractArray{X, N}; kw_args...) where {X, N} T(convert(AbstractArray{gl_promote(X), N}, a); kw_args...) diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 2b7170401b3..31d32884d27 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -2,6 +2,100 @@ using Makie: transform_func_obs, apply_transform using Makie: attribute_per_char, FastPixel, el32convert, Pixel using Makie: convert_arguments +function handle_lights(attr::Dict, screen::Screen, lights::Vector{Makie.AbstractLight}) + @inline function push_inplace!(trg, idx, src) + for i in eachindex(src) + trg[idx + i] = src[i] + end + return idx + length(src) + end + + MAX_LIGHTS = screen.config.max_lights + MAX_PARAMS = screen.config.max_light_parameters + + # Every light has a type and a color. Therefore we have these as independent + # uniforms with a max length of MAX_LIGHTS. + # Other parameters like position, direction, etc differe between light types. + # To avoid wasting a bunch of memory we squash all of them into one vector of + # size MAX_PARAMS. + attr[:N_lights] = Observable(0) + attr[:light_types] = Observable(sizehint!(Int32[], MAX_LIGHTS)) + attr[:light_colors] = Observable(sizehint!(RGBf[], MAX_LIGHTS)) + attr[:light_parameters] = Observable(sizehint!(Float32[], MAX_PARAMS)) + + on(screen.render_tick, priority = typemin(Int)) do _ + # derive number of lights from available lights. Both MAX_LIGHTS and + # MAX_PARAMS are considered for this. + n_lights = 0 + n_params = 0 + for light in lights + delta = 0 + if light isa PointLight + delta = 5 # 3 position + 2 attenuation + elseif light isa DirectionalLight + delta = 3 # 3 direction + elseif light isa SpotLight + delta = 8 # 3 position + 3 direction + 2 angles + end + if n_params + delta > MAX_PARAMS || n_lights == MAX_LIGHTS + if n_params > MAX_PARAMS + @warn "Exceeded the maximum number of light parameters ($n_params > $MAX_PARAMS). Skipping lights beyond number $n_lights." + else + @warn "Exceeded the maximum number of lights ($n_lights > $MAX_LIGHTS). Skipping lights beyond number $n_lights." + end + break + end + n_params += delta + n_lights += 1 + end + + # Update number of lights + attr[:N_lights][] = n_lights + + # Update light types + trg = attr[:light_types][] + resize!(trg, n_lights) + map!(i -> Makie.light_type(lights[i]), trg, 1:n_lights) + notify(attr[:light_types]) + + # Update light colors + trg = attr[:light_colors][] + resize!(trg, n_lights) + map!(i -> Makie.light_color(lights[i]), trg, 1:n_lights) + notify(attr[:light_colors]) + + # Update other light parameters + # This precalculates world space pos/dir -> view/cam space pos/dir + parameters = attr[:light_parameters][] + resize!(parameters, n_params) + idx = 0 + for i in 1:n_lights + light = lights[i] + if light isa PointLight + idx = push_inplace!(parameters, idx, light.position[]) + idx = push_inplace!(parameters, idx, light.attenuation[]) + elseif light isa DirectionalLight + if light.camera_relative + T = inv(attr[:view][][Vec(1,2,3), Vec(1,2,3)]) + dir = normalize(T * light.direction[]) + else + dir = normalize(light.direction[]) + end + idx = push_inplace!(parameters, idx, dir) + elseif light isa SpotLight + idx = push_inplace!(parameters, idx, light.position[]) + idx = push_inplace!(parameters, idx, normalize(light.direction[])) + idx = push_inplace!(parameters, idx, cos.(light.angles[])) + end + end + notify(attr[:light_parameters]) + + return Consume(false) + end + + return attr +end + Makie.el32convert(x::GLAbstraction.Texture) = x gpuvec(x) = GPUVector(GLBuffer(x)) @@ -48,7 +142,17 @@ function connect_camera!(plot, gl_attributes, cam, space = gl_attributes[:space] end # end end - get!(gl_attributes, :normalmatrix) do + + # for lighting + get!(gl_attributes, :world_normalmatrix) do + return lift(plot, gl_attributes[:model]) do m + i = Vec(1, 2, 3) + return transpose(inv(m[i, i])) + end + end + + # for SSAO + get!(gl_attributes, :view_normalmatrix) do return lift(plot, gl_attributes[:view], gl_attributes[:model]) do v, m i = Vec(1, 2, 3) return transpose(inv(v[i, i] * m[i, i])) @@ -144,22 +248,48 @@ function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) gl_attributes[:markerspace] = plot.markerspace end gl_attributes[:space] = plot.space - - pointlight = Makie.get_point_light(scene) - if !isnothing(pointlight) - gl_attributes[:lightposition] = pointlight.position - end - - ambientlight = Makie.get_ambient_light(scene) - if !isnothing(ambientlight) - gl_attributes[:ambient] = ambientlight.color - end gl_attributes[:px_per_unit] = screen.px_per_unit handle_intensities!(screen, gl_attributes, plot) connect_camera!(plot, gl_attributes, scene.camera, get_space(plot)) - robj = robj_func(gl_attributes) + # TODO: remove depwarn & conversion after some time + if haskey(gl_attributes, :shading) && to_value(gl_attributes[:shading]) isa Bool + @warn "`shading::Bool` is deprecated. Use `shading = NoShading` instead of false and `shading = FastShading` or `shading = MultiLightShading` instead of true." + gl_attributes[:shading] = ifelse(gl_attributes[:shading][], FastShading, NoShading) + elseif haskey(gl_attributes, :shading) && gl_attributes[:shading] isa Observable + gl_attributes[:shading] = gl_attributes[:shading][] + end + + shading = to_value(get(gl_attributes, :shading, NoShading)) + + if shading == FastShading + dirlight = Makie.get_directional_light(scene) + if !isnothing(dirlight) + gl_attributes[:light_direction] = if dirlight.camera_relative + map(gl_attributes[:view], dirlight.direction) do view, dir + return normalize(inv(view[Vec(1,2,3), Vec(1,2,3)]) * dir) + end + else + map(normalize, dirlight.direction) + end + + gl_attributes[:light_color] = dirlight.color + else + gl_attributes[:light_direction] = Observable(Vec3f(0)) + gl_attributes[:light_color] = Observable(RGBf(0,0,0)) + end + + ambientlight = Makie.get_ambient_light(scene) + if !isnothing(ambientlight) + gl_attributes[:ambient] = ambientlight.color + else + gl_attributes[:ambient] = Observable(RGBf(0,0,0)) + end + elseif shading == MultiLightShading + handle_lights(gl_attributes, screen, scene.lights) + end + robj = robj_func(gl_attributes) # <-- here get!(gl_attributes, :ssao, Observable(false)) screen.cache2plot[robj.id] = plot @@ -235,7 +365,6 @@ pixel2world(scene, msize::AbstractVector) = pixel2world.(scene, msize) function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Union{Scatter, MeshScatter})) return cached_robj!(screen, scene, plot) do gl_attributes # signals not supported for shading yet - gl_attributes[:shading] = to_value(get(gl_attributes, :shading, true)) marker = pop!(gl_attributes, :marker) space = plot.space @@ -379,7 +508,7 @@ function draw_atomic(screen::Screen, scene::Scene, # calculate quad metrics glyph_data = lift(plot, pos, glyphcollection, offset, transfunc, space) do pos, gc, offset, transfunc, space - Makie.text_quads(atlas, pos, to_value(gc), offset, transfunc, space) + return Makie.text_quads(atlas, pos, to_value(gc), offset, transfunc, space) end # unpack values from the one signal: @@ -503,7 +632,7 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Image) gl_attributes[:texturecoordinates] = map(decompose_uv(rect)) do uv return 1.0f0 .- Vec2f(uv[2], uv[1]) end - gl_attributes[:shading] = false + get!(gl_attributes, :shading, NoShading) _interp = to_value(pop!(gl_attributes, :interpolate, true)) interp = _interp ? :linear : :nearest if haskey(gl_attributes, :intensity) @@ -517,8 +646,8 @@ end function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space=:data) # signals not supported for shading yet - shading = to_value(pop!(gl_attributes, :shading)) - gl_attributes[:shading] = shading + shading = to_value(gl_attributes[:shading])::Makie.MakieCore.ShadingAlgorithm + matcap_active = !isnothing(to_value(get(gl_attributes, :matcap, nothing))) color = pop!(gl_attributes, :color) interp = to_value(pop!(gl_attributes, :interpolate, true)) interp = interp ? :linear : :nearest @@ -560,25 +689,27 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space= if hasproperty(to_value(mesh), :uv) gl_attributes[:texturecoordinates] = lift(decompose_uv, mesh) end - if hasproperty(to_value(mesh), :normals) && shading + if hasproperty(to_value(mesh), :normals) && (shading !== NoShading || matcap_active) gl_attributes[:normals] = lift(decompose_normals, mesh) end return draw_mesh(screen, gl_attributes) end function draw_atomic(screen::Screen, scene::Scene, meshplot::Mesh) - return cached_robj!(screen, scene, meshplot) do gl_attributes + x = cached_robj!(screen, scene, meshplot) do gl_attributes t = transform_func_obs(meshplot) space = meshplot.space # needs to happen before connect_camera! call - return mesh_inner(screen, meshplot[1], t, gl_attributes, meshplot, space) + x = mesh_inner(screen, meshplot[1], t, gl_attributes, meshplot, space) + return x end + + return x end function draw_atomic(screen::Screen, scene::Scene, plot::Surface) robj = cached_robj!(screen, scene, plot) do gl_attributes color = pop!(gl_attributes, :color) img = nothing - # signals not supported for shading yet # We automatically insert x[3] into the color channel, so if it's equal we don't need to do anything if haskey(gl_attributes, :intensity) img = pop!(gl_attributes, :intensity) @@ -599,7 +730,6 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Surface) space = plot.space gl_attributes[:image] = img - gl_attributes[:shading] = to_value(get(gl_attributes, :shading, true)) @assert to_value(plot[3]) isa AbstractMatrix types = map(v -> typeof(to_value(v)), plot[1:2]) diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index 5010ba52746..52f631095ce 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -194,7 +194,7 @@ function (p::MousePositionUpdater)(::Nothing) @print_error p.mouseposition[] = pos # notify!(e.mouseposition) end - return + return Consume(false) end """ @@ -208,7 +208,7 @@ function Makie.mouse_position(scene::Scene, screen::Screen) updater = MousePositionUpdater( screen, scene.events.mouseposition, scene.events.hasfocus ) - on(updater, scene, screen.render_tick) + on(updater, scene, screen.render_tick, priority = typemax(Int)) return end function Makie.disconnect!(screen::Screen, ::typeof(mouse_position)) diff --git a/GLMakie/src/glshaders/image_like.jl b/GLMakie/src/glshaders/image_like.jl index 97c6fa990e8..0fd7ccbaa05 100644 --- a/GLMakie/src/glshaders/image_like.jl +++ b/GLMakie/src/glshaders/image_like.jl @@ -34,6 +34,7 @@ A matrix of Intensities will result in a contourf kind of plot function draw_heatmap(screen, data::Dict) primitive = triangle_mesh(Rect2(0f0,0f0,1f0,1f0)) to_opengl_mesh!(data, primitive) + pop!(data, :shading, FastShading) @gen_defaults! data begin intensity = nothing => Texture color_map = nothing => Texture @@ -55,6 +56,8 @@ end function draw_volume(screen, main::VolumeTypes, data::Dict) geom = Rect3f(Vec3f(0), Vec3f(1)) to_opengl_mesh!(data, const_lift(GeometryBasics.triangle_mesh, geom)) + shading = pop!(data, :shading, FastShading) + pop!(data, :backlight, 0f0) # We overwrite this @gen_defaults! data begin volumedata = main => Texture model = Mat4f(I) @@ -67,12 +70,17 @@ function draw_volume(screen, main::VolumeTypes, data::Dict) absorption = 1f0 isovalue = 0.5f0 isorange = 0.01f0 + backlight = 1f0 enable_depth = true transparency = false shader = GLVisualizeShader( screen, - "fragment_output.frag", "util.vert", "volume.vert", "volume.frag", + "util.vert", "volume.vert", + "fragment_output.frag", "lighting.frag", "volume.frag", view = Dict( + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "depth_init" => vol_depth_init(to_value(enable_depth)), "depth_default" => vol_depth_default(to_value(enable_depth)), "depth_main" => vol_depth_main(to_value(enable_depth)), diff --git a/GLMakie/src/glshaders/mesh.jl b/GLMakie/src/glshaders/mesh.jl index 977cbc57c2b..877ddf90dea 100644 --- a/GLMakie/src/glshaders/mesh.jl +++ b/GLMakie/src/glshaders/mesh.jl @@ -27,7 +27,9 @@ function to_opengl_mesh!(result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) to_buffer(:uv, :texturecoordinates) to_buffer(:uvw, :texturecoordinates) # Only emit normals, when we shadin' - if to_value(get(result, :shading, true)) || !isnothing(to_value(get(result, :matcap, nothing))) + shading = get(result, :shading, NoShading)::Makie.MakieCore.ShadingAlgorithm + matcap_active = !isnothing(to_value(get(result, :matcap, nothing))) + if matcap_active || shading != NoShading to_buffer(:normals, :normals) end to_buffer(:attribute_id, :attribute_id) @@ -35,11 +37,11 @@ function to_opengl_mesh!(result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) end function draw_mesh(screen, data::Dict) + shading = pop!(data, :shading, NoShading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin vertices = nothing => GLBuffer faces = nothing => indexbuffer normals = nothing => GLBuffer - shading = true backlight = 0f0 vertex_color = nothing => GLBuffer image = nothing => Texture @@ -53,9 +55,13 @@ function draw_mesh(screen, data::Dict) interpolate_in_fragment_shader = true shader = GLVisualizeShader( screen, - "util.vert", "mesh.vert", "mesh.frag", "fragment_output.frag", + "util.vert", "mesh.vert", + "fragment_output.frag", "mesh.frag", + "lighting.frag", view = Dict( - "light_calc" => light_calc(shading), + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "buffers" => output_buffers(screen, to_value(transparency)), "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index f44bec57305..90e3aab1575 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -57,9 +57,9 @@ function draw_mesh_particle(screen, p, data) scale = Vec3f(1) => TextureBuffer rotation = rot => TextureBuffer texturecoordinates = nothing - shading = true end + shading = pop!(data, :shading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin color_map = nothing => Texture color_norm = nothing @@ -71,16 +71,19 @@ function draw_mesh_particle(screen, p, data) fetch_pixel = false interpolate_in_fragment_shader = false uv_scale = Vec2f(1) + backlight = 0f0 instances = const_lift(length, position) - shading = true transparency = false shader = GLVisualizeShader( screen, - "util.vert", "particles.vert", "mesh.frag", "fragment_output.frag", + "util.vert", "particles.vert", + "fragment_output.frag", "lighting.frag", "mesh.frag", view = Dict( "position_calc" => position_calc(position, nothing, nothing, nothing, TextureBuffer), - "light_calc" => light_calc(shading), + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "buffers" => output_buffers(screen, to_value(transparency)), "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) diff --git a/GLMakie/src/glshaders/surface.jl b/GLMakie/src/glshaders/surface.jl index 9750cd458e4..da3d678aa96 100644 --- a/GLMakie/src/glshaders/surface.jl +++ b/GLMakie/src/glshaders/surface.jl @@ -12,17 +12,23 @@ function normal_calc(x::Bool, invert_normals::Bool = false) end end +# TODO this shouldn't be necessary function light_calc(x::Bool) - if x - """ - vec3 L = normalize(o_lightdir); - vec3 N = normalize(o_normal); - vec3 light1 = blinnphong(N, o_camdir, L, color.rgb); - vec3 light2 = blinnphong(N, o_camdir, -L, color.rgb); - color = vec4(ambient * color.rgb + light1 + backlight * light2, color.a); - """ + @error "shading::Bool is deprecated. Use `NoShading` instead of `false` and `FastShading` or `MultiLightShading` instead of true." + return light_calc(ifelse(x, FastShading, NoShading)) +end + +function light_calc(x::Makie.MakieCore.ShadingAlgorithm) + if x === NoShading + return "#define NO_SHADING" + elseif x === FastShading + return "#define FAST_SHADING" + elseif x === MultiLightShading + return "#define MULTI_LIGHT_SHADING" + # elseif x === :PBR # TODO? else - "" + @warn "Did not recognize shading value :$x. Defaulting to FastShading." + return "#define FAST_SHADING" end end @@ -113,6 +119,7 @@ end function draw_surface(screen, main, data::Dict) primitive = triangle_mesh(Rect2(0f0,0f0,1f0,1f0)) to_opengl_mesh!(data, primitive) + shading = pop!(data, :shading, FastShading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin scale = nothing position = nothing @@ -120,8 +127,7 @@ function draw_surface(screen, main, data::Dict) position_y = nothing => Texture position_z = nothing => Texture image = nothing => Texture - shading = true - normal = shading + normal = shading != NoShading invert_normals = false backlight = 0f0 end @@ -141,12 +147,14 @@ function draw_surface(screen, main, data::Dict) transparency = false shader = GLVisualizeShader( screen, - "fragment_output.frag", "util.vert", "surface.vert", - "mesh.frag", + "util.vert", "surface.vert", + "fragment_output.frag", "lighting.frag", "mesh.frag", view = Dict( "position_calc" => position_calc(position, position_x, position_y, position_z, Texture), "normal_calc" => normal_calc(normal, to_value(invert_normals)), - "light_calc" => light_calc(shading), + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "buffers" => output_buffers(screen, to_value(transparency)), "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) diff --git a/GLMakie/src/glshaders/visualize_interface.jl b/GLMakie/src/glshaders/visualize_interface.jl index 1633084e6b6..d9236e4f61c 100644 --- a/GLMakie/src/glshaders/visualize_interface.jl +++ b/GLMakie/src/glshaders/visualize_interface.jl @@ -176,7 +176,7 @@ function output_buffer_writes(screen::Screen, transparency = false) """ fragment_color = color; fragment_position = o_view_pos; - fragment_normal_occlusion.xyz = o_normal; + fragment_normal_occlusion.xyz = o_view_normal; """ else "fragment_color = color;" diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 64a2e1b2ffa..54303343909 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -30,12 +30,14 @@ function renderloop end * `visible = true`: Whether or not the window should be visible when first created. * `scalefactor = automatic`: Sets the window scaling factor, such as `2.0` on HiDPI/Retina displays. It is set automatically based on the display, but may be any positive real number. -## Postprocessor +## Rendering constants & Postprocessor * `oit = false`: Whether to enable order independent transparency for the window. * `fxaa = true`: Whether to enable fxaa (anti-aliasing) for the window. * `ssao = true`: Whether to enable screen space ambient occlusion, which simulates natural shadowing at inner edges and crevices. * `transparency_weight_scale = 1000f0`: Adjusts a factor in the rendering shaders for order independent transparency. This should be the same for all of them (within one rendering pipeline) otherwise depth "order" will be broken. +* `max_lights = 64`: The maximum number of lights with `shading = MultiLightShading` +* `max_light_parameters = 5 * N_lights`: The maximum number of light parameters that can be uploaded. These include everything other than the light color (i.e. position, direction, attenuation, angles) in terms of scalar floats. """ mutable struct ScreenConfig # Renderloop @@ -57,11 +59,13 @@ mutable struct ScreenConfig visible::Bool scalefactor::Union{Nothing, Float32} - # Postprocessor + # Render Constants & Postprocessor oit::Bool fxaa::Bool ssao::Bool transparency_weight_scale::Float32 + max_lights::Int + max_light_parameters::Int function ScreenConfig( # Renderloop @@ -86,7 +90,9 @@ mutable struct ScreenConfig oit::Bool, fxaa::Bool, ssao::Bool, - transparency_weight_scale::Number) + transparency_weight_scale::Number, + max_lights::Int, + max_light_parameters::Int) return new( # Renderloop renderloop isa Makie.Automatic ? GLMakie.renderloop : renderloop, @@ -110,7 +116,9 @@ mutable struct ScreenConfig oit, fxaa, ssao, - transparency_weight_scale) + transparency_weight_scale, + max_lights, + max_light_parameters) end end @@ -164,7 +172,7 @@ mutable struct Screen{GLWindow} <: MakieScreen cache::Dict{UInt64, RenderObject} cache2plot::Dict{UInt32, AbstractPlot} framecache::Matrix{RGB{N0f8}} - render_tick::Observable{Nothing} + render_tick::Observable{Nothing} # listeners must not Consume(true) window_open::Observable{Bool} scalefactor::Observable{Float32} @@ -354,6 +362,9 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B replace_processor!(config.ssao ? ssao_postprocessor : empty_postprocessor, 1) replace_processor!(config.oit ? OIT_postprocessor : empty_postprocessor, 2) replace_processor!(config.fxaa ? fxaa_postprocessor : empty_postprocessor, 3) + + # TODO: replace shader programs with lighting to update N_lights & N_light_parameters + # Set the config screen.config = config if start_renderloop @@ -433,8 +444,9 @@ end function pollevents(screen::Screen) ShaderAbstractions.switch_context!(screen.glscreen) - notify(screen.render_tick) GLFW.PollEvents() + notify(screen.render_tick) + return end Base.wait(x::Screen) = !isnothing(x.rendertask) && wait(x.rendertask) @@ -713,7 +725,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma ctex = screen.framebuffer.buffers[:color] # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! - # GLFW.PollEvents() + pollevents(screen) # keep current buffer size to allows larger-than-window renders render_frame(screen, resize_buffers=false) # let it render if screen.config.visible diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 1f444752afa..2ffccb213de 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -103,3 +103,45 @@ end mesh!(right, Sphere(Point3f(5), 6.0f0); color=:black) fig end + +@reference_test "Complex Lighting - Ambient + SpotLights + PointLights" begin + angle2pos(phi) = Point3f(cosd(phi), sind(phi), 0) + lights = [ + AmbientLight(RGBf(0.1, 0.1, 0.1)), + SpotLight(RGBf(2,0,0), angle2pos(0), Vec3f(0, 0, -1), Vec2f(pi/5, pi/4)), + SpotLight(RGBf(0,2,0), angle2pos(120), Vec3f(0, 0, -1), Vec2f(pi/5, pi/4)), + SpotLight(RGBf(0,0,2), angle2pos(240), Vec3f(0, 0, -1), Vec2f(pi/5, pi/4)), + PointLight(RGBf(1,1,1), Point3f(-4, -4, -2.5), 10.0), + PointLight(RGBf(1,1,0), Point3f(-4, 4, -2.5), 10.0), + PointLight(RGBf(1,0,1), Point3f( 4, 4, -2.5), 10.0), + PointLight(RGBf(0,1,1), Point3f( 4, -4, -2.5), 10.0), + ] + + scene = Scene(resolution = (400, 400), camera = cam3d!, lights = lights) + mesh!( + scene, + Rect3f(Point3f(-10, -10, -2.99), Vec3f(20, 20, 0.02)), + color = :white, shading = MultiLightShading, specular = Vec3f(0) + ) + center!(scene) + update_cam!(scene, Vec3f(0, 0, 10), Vec3f(0, 0, 0), Vec3f(0, 1, 0)) + scene +end + +@reference_test "Complex Lighting - DirectionalLight + specular reflection" begin + angle2dir(phi) = Vec3f(cosd(phi), sind(phi), -2) + lights = [ + AmbientLight(RGBf(0.1, 0.1, 0.1)), + DirectionalLight(RGBf(1,0,0), angle2dir(0)), + DirectionalLight(RGBf(0,1,0), angle2dir(120)), + DirectionalLight(RGBf(0,0,1), angle2dir(240)), + ] + + scene = Scene(resolution = (400, 400), camera = cam3d!, center = false, lights = lights, backgroundcolor = :black) + mesh!( + scene, Sphere(Point3f(0), 1f0), color = :white, shading = MultiLightShading, + specular = Vec3f(1), shininess = 16f0 + ) + update_cam!(scene, Vec3f(0, 0, 3), Vec3f(0, 0, 0), Vec3f(0, 1, 0)) + scene +end diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 05c4f31ff71..ec263145dcf 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -250,7 +250,7 @@ end @test screen.root_scene === nothing @test screen.rendertask === nothing - @test (Base.summarysize(screen) / 10^6) < 1.2 + @test (Base.summarysize(screen) / 10^6) < 1.21 end # All should go to pool after close @test all(x-> x in GLMakie.SCREEN_REUSE_POOL, screens) diff --git a/MakieCore/src/basic_plots.jl b/MakieCore/src/basic_plots.jl index dad4f98ef28..b755c41b241 100644 --- a/MakieCore/src/basic_plots.jl +++ b/MakieCore/src/basic_plots.jl @@ -79,16 +79,17 @@ end """ ### 3D shading attributes -- `shading = true` enables lighting. -- `diffuse::Vec3f = Vec3f(0.4)` sets how strongly the red, green and blue channel react to diffuse (scattered) light. -- `specular::Vec3f = Vec3f(0.2)` sets how strongly the object reflects light in the red, green and blue channels. +- `shading = automatic` sets the lighting algorithm used. Options are `NoShading` (no lighting), `FastShading` (AmbientLight + PointLight) or `MultiLightShading` (Multiple lights, GLMakie only). Note that this does not affect RPRMakie. +- `diffuse::Vec3f = Vec3f(1.0)` sets how strongly the red, green and blue channel react to diffuse (scattered) light. +- `specular::Vec3f = Vec3f(0.4)` sets how strongly the object reflects light in the red, green and blue channels. - `shininess::Real = 32.0` sets how sharp the reflection is. +- `backlight::Float32 = 0f0` sets a weight for secondary light calculation with inverted normals. - `ssao::Bool = false` adjusts whether the plot is rendered with ssao (screen space ambient occlusion). Note that this only makes sense in 3D plots and is only applicable with `fxaa = true`. """ function shading_attributes!(attr) - attr[:shading] = true - attr[:diffuse] = 0.4 - attr[:specular] = 0.2 + attr[:shading] = automatic + attr[:diffuse] = 1.0 + attr[:specular] = 0.4 attr[:shininess] = 32.0f0 attr[:backlight] = 0f0 attr[:ssao] = false @@ -537,7 +538,7 @@ $(Base.Docs.doc(MakieCore.generic_plot_attributes!)) strokewidth = theme(scene, :patchstrokewidth), linestyle = nothing, - shading = false, + shading = NoShading, fxaa = true, cycle = [:color => :patchcolor], diff --git a/MakieCore/src/types.jl b/MakieCore/src/types.jl index 7dba1bf60ea..60a40aef6a1 100644 --- a/MakieCore/src/types.jl +++ b/MakieCore/src/types.jl @@ -122,3 +122,9 @@ end Billboard() = Billboard(0f0) Billboard(angle::Real) = Billboard(Float32(angle)) Billboard(angles::Vector) = Billboard(Float32.(angles)) + +@enum ShadingAlgorithm begin + NoShading + FastShading + MultiLightShading +end \ No newline at end of file diff --git a/NEWS.md b/NEWS.md index 8d5df5b6d93..32c76bade84 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,8 +8,10 @@ - Fixed an issue where NaN was interpreted as zero when rendering `surface` through CairoMakie. [#2598](https://github.com/MakieOrg/Makie.jl/pull/2598) - Improved 3D camera handling, hotkeys and functionality [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) - Refactored the `SurfaceLike` family of traits into `VertexBasedGrid`, `CellBasedGrid` and `ImageLike`. [#3106](https://github.com/MakieOrg/Makie.jl/pull/3106) +- Added `shading = :verbose` in GLMakie to allow for multiple light sources. Also added more light types, fixed light directions for the previous lighting model (now `shading = :fast`) and adjusted `backlight` to affect normals. [#3246](https://github.com/MakieOrg/Makie.jl/pull/3246) ## master + - Fix grouping of a zero-height bar in `barplot`. Now a zero-height bar shares the same properties of the previous bar, and if the bar is the first one, its height is treated as positive if and only if there exists a bar of positive height or all bars are zero-height. [#3058](https://github.com/MakieOrg/Makie.jl/pull/3058) - Fixed a bug where Axis still consumes scroll events when interactions are disabled [#3272](https://github.com/MakieOrg/Makie.jl/pull/3272) - Added `cornerradius` attribute to `Box` for rounded corners [#3308](https://github.com/MakieOrg/Makie.jl/pull/3308). @@ -23,8 +25,8 @@ ## v0.19.10 -- Fix bugs with Colorbar in recipes, add new API for creating a recipe colorbar and introduce experimental support for Categorical colormaps [#3090](https://github.com/MakieOrg/Makie.jl/pull/3090). -- Add experimental Datashader implementation [#2883](https://github.com/MakieOrg/Makie.jl/pull/2883). +- Fixed bugs with Colorbar in recipes, add new API for creating a recipe colorbar and introduce experimental support for Categorical colormaps [#3090](https://github.com/MakieOrg/Makie.jl/pull/3090). +- Added experimental Datashader implementation [#2883](https://github.com/MakieOrg/Makie.jl/pull/2883). - [Breaking] Changed the default order Polar arguments to (theta, r). [#3154](https://github.com/MakieOrg/Makie.jl/pull/3154) - General improvements to `PolarAxis`: full rlimtis & thetalimits, more controls and visual tweaks. See pr for more details.[#3154](https://github.com/MakieOrg/Makie.jl/pull/3154) diff --git a/RPRMakie/src/scene.jl b/RPRMakie/src/scene.jl index ea76382db46..5001a1fa809 100644 --- a/RPRMakie/src/scene.jl +++ b/RPRMakie/src/scene.jl @@ -43,17 +43,60 @@ function insert_plots!(context, matsys, scene, mscene::Makie.Scene, @nospecializ end end +@inline to_rpr_light(ctx, light, scene) = to_rpr_light(ctx, light) + +# TODO attenuation function to_rpr_light(context::RPR.Context, light::Makie.PointLight) pointlight = RPR.PointLight(context) map(light.position) do pos transform!(pointlight, Makie.translationmatrix(pos)) end - map(light.radiance) do r - setradiantpower!(pointlight, red(r), green(r), blue(r)) + map(light.color) do c + setradiantpower!(pointlight, red(c), green(c), blue(c)) end return pointlight end +# TODO: Move to RadeonProRender.jl +function RPR.RPR.rprContextCreateSpotLight(context) + out_light = Ref{RPR.rpr_light}() + RPR.RPR.rprContextCreateSpotLight(context, out_light) + return out_light[] +end + +function to_rpr_light(context::RPR.Context, light::Makie.DirectionalLight, scene) + directionallight = RPR.DirectionalLight(context) + map(light.direction) do dir + if light.camera_relative + T = inv(scene.camera.view[][Vec(1,2,3), Vec(1,2,3)]) + dir = normalize(T * dir) + else + dir = normalize(dir) + end + quart = Makie.rotation_between(dir, Vec3f(0,0,-1)) + transform!(directionallight, Makie.rotationmatrix4(quart)) + end + map(light.color) do c + setradiantpower!(directionallight, red(c), green(c), blue(c)) + end + return directionallight +end + +function to_rpr_light(context::RPR.Context, light::Makie.SpotLight) + spotlight = RPR.SpotLight(context) + map(light.position, light.direction) do pos, dir + quart = Makie.rotation_between(dir, Vec3f(0,0,-1)) + transform!(spotlight, Makie.translationmatrix(pos) * Makie.rotationmatrix4(quart)) + end + map(light.color) do c + setradiantpower!(spotlight, red(c), green(c), blue(c)) + end + map(light.angles) do (inner, outer) + RadeonProRender.RPR.rprSpotLightSetConeShape(spotlight, inner, outer) + end + return spotlight +end + function to_rpr_light(context::RPR.Context, light::Makie.AmbientLight) env_img = fill(light.color[], 1, 1) img = RPR.Image(context, env_img) @@ -90,7 +133,7 @@ function to_rpr_scene(context::RPR.Context, matsys, mscene::Makie.Scene) RPR.rprSceneSetBackgroundImage(scene, img) end for light in mscene.lights - rpr_light = to_rpr_light(context, light) + rpr_light = to_rpr_light(context, light, mscene) push!(scene, rpr_light) end diff --git a/ReferenceTests/src/tests/attributes.jl b/ReferenceTests/src/tests/attributes.jl index 845a1d3e7e0..5b89c3d86bd 100644 --- a/ReferenceTests/src/tests/attributes.jl +++ b/ReferenceTests/src/tests/attributes.jl @@ -27,7 +27,7 @@ end end @reference_test "shading" begin - mesh(Sphere(Point3f(0), 1f0), color=:orange, shading=false) + mesh(Sphere(Point3f(0), 1f0), color=:orange, shading=NoShading) end @reference_test "visible" begin diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index fb10a5197f6..62991506bdf 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -110,7 +110,7 @@ end 5 8 9; ] color = [0.0, 0.0, 0.0, 0.0, -0.375, 0.0, 0.0, 0.0, 0.0] - fig, ax, meshplot = mesh(coordinates, connectivity, color=color, shading=false) + fig, ax, meshplot = mesh(coordinates, connectivity, color=color, shading=NoShading) wireframe!(ax, meshplot[1], color=(:black, 0.6), linewidth=3) fig end @@ -118,7 +118,7 @@ end @reference_test "colored triangle" begin mesh( [(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)], color=[:red, :green, :blue], - shading=false + shading=NoShading ) end @@ -435,7 +435,7 @@ end s = 1.5scales[i] mesh!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, - shading = false, color = :blue) + shading = NoShading, color = :blue) lines!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, linewidth = 2, color = :red) @@ -477,7 +477,7 @@ end s = 1.5scales[i] mesh!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, - shading = false, color = :blue) + shading = NoShading, color = :blue) lines!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, linewidth = 2, color = :red) @@ -530,7 +530,7 @@ end end @reference_test "2D surface with explicit color" begin - surface(1:10, 1:10, ones(10, 10); color = [RGBf(x*y/100, 0, 0) for x in 1:10, y in 1:10], shading = false) + surface(1:10, 1:10, ones(10, 10); color = [RGBf(x*y/100, 0, 0) for x in 1:10, y in 1:10], shading = NoShading) end @reference_test "heatmap and image colormap interpolation" begin diff --git a/ReferenceTests/src/tests/examples3d.jl b/ReferenceTests/src/tests/examples3d.jl index 32f313657cb..410aa07c5a8 100644 --- a/ReferenceTests/src/tests/examples3d.jl +++ b/ReferenceTests/src/tests/examples3d.jl @@ -1,7 +1,7 @@ @reference_test "Image on Geometry (Moon)" begin moon = loadasset("moon.png") - fig, ax, meshplot = mesh(Sphere(Point3f(0), 1f0), color=moon, shading=false, axis = (;show_axis=false)) + fig, ax, meshplot = mesh(Sphere(Point3f(0), 1f0), color=moon, shading=NoShading, axis = (;show_axis=false)) update_cam!(ax.scene, Vec3f(-2, 2, 2), Vec3f(0)) fig end @@ -9,7 +9,7 @@ end @reference_test "Image on Geometry (Earth)" begin earth = loadasset("earth.png") m = uv_mesh(Tesselation(Sphere(Point3f(0), 1f0), 60)) - mesh(m, color=earth, shading=false) + mesh(m, color=earth, shading=NoShading) end @reference_test "Orthographic Camera" begin @@ -31,6 +31,7 @@ end scene = ax.scene cam = cameracontrols(scene) cam.settings[:projectiontype][] = Makie.Orthographic + cam.settings.center[] = false # This would be set by update_cam!() cam.upvector[] = (0.0, 0.0, 1.0) cam.lookat[] = Vec3f(0.595, 2.5, 0.5) cam.eyeposition[] = (cam.lookat[][1], cam.lookat[][2] + 0.61, cam.lookat[][3]) @@ -593,7 +594,7 @@ end fig = Figure() for ax in [LScene(fig[1, 1]), Axis3(fig[1, 2])] mesh!(ax, Rect3(Point3f(-10), Vec3f(20)), color = :orange) - mesh!(ax, Rect2f(0.8, 0.1, 0.1, 0.8), space = :relative, color = :blue, shading = false) + mesh!(ax, Rect2f(0.8, 0.1, 0.1, 0.8), space = :relative, color = :blue, shading = NoShading) linesegments!(ax, Rect2f(-0.5, -0.5, 1, 1), space = :clip, color = :cyan, linewidth = 5) text!(ax, 0, 0.52, text = "Clip Space", align = (:center, :bottom), space = :clip) image!(ax, 0..40, 0..800, [x for x in range(0, 1, length=40), _ in 1:10], space = :pixel) diff --git a/ReferenceTests/src/tests/figures_and_makielayout.jl b/ReferenceTests/src/tests/figures_and_makielayout.jl index bd51a2130c8..ac01eb406d0 100644 --- a/ReferenceTests/src/tests/figures_and_makielayout.jl +++ b/ReferenceTests/src/tests/figures_and_makielayout.jl @@ -51,7 +51,7 @@ end @reference_test "Label with text wrapping" begin lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." fig = Figure(resolution = (1000, 660)) - m!(fig, lbl) = mesh!(fig.scene, lbl.layoutobservables.computedbbox, color = (:red, 0.5), shading=false) + m!(fig, lbl) = mesh!(fig.scene, lbl.layoutobservables.computedbbox, color = (:red, 0.5), shading=NoShading) lbl1 = Label(fig[1, 1:2], "HEADER "^10, fontsize = 40, word_wrap = true) m!(fig, lbl1) @@ -141,7 +141,7 @@ end f = Figure() ax = PolarAxis(f[1, 1]) zs = [r*cos(phi) for phi in range(0, 4pi, length=100), r in range(1, 2, length=100)] - p = surface!(ax, 0..2pi, 0..10, zs, shading = false, colormap = :coolwarm, colorrange=(-2, 2)) + p = surface!(ax, 0..2pi, 0..10, zs, shading = NoShading, colormap = :coolwarm, colorrange=(-2, 2)) rlims!(ax, 0, 11) # verify that r = 10 doesn't end up at r > 10 translate!(p, 0, 0, -200) Colorbar(f[1, 2], p) diff --git a/ReferenceTests/src/tests/short_tests.jl b/ReferenceTests/src/tests/short_tests.jl index c939fc60626..6301839541e 100644 --- a/ReferenceTests/src/tests/short_tests.jl +++ b/ReferenceTests/src/tests/short_tests.jl @@ -132,7 +132,7 @@ end highclip = :red, lowclip = :black, nan_color = (:green, 0.5), - shading = false, + shading = NoShading, ) surface!( Axis(fig[2, 2]), @@ -141,7 +141,7 @@ end highclip = :red, lowclip = :black, nan_color = (:green, 0.5), - shading = false, + shading = NoShading, ) fig end diff --git a/WGLMakie/assets/mesh.frag b/WGLMakie/assets/mesh.frag index 1355d41e6b6..efe137428e3 100644 --- a/WGLMakie/assets/mesh.frag +++ b/WGLMakie/assets/mesh.frag @@ -4,19 +4,31 @@ flat in int sample_frag_color; in vec3 o_normal; in vec3 o_camdir; -in vec3 o_lightdir; + +// Smoothes out edge around 0 light intensity, see GLMakie +float smooth_zero_max(float x) { + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + float pow8 = x + shift; + pow8 = pow8 * pow8; pow8 = pow8 * pow8; pow8 = pow8 * pow8; + return x < yswap ? c * pow8 : x; +} vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0); + float backlight = get_backlight(); + float diff_coeff = smooth_zero_max(dot(L, -N)) + + backlight * smooth_zero_max(dot(L, N)); // specular coefficient vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0), get_shininess()); + float spec_coeff = pow(max(dot(H, -N), 0.0), get_shininess()) + + backlight * pow(max(dot(H, N), 0.0), get_shininess()); if (diff_coeff <= 0.0) spec_coeff = 0.0; + // final lighting model - return vec3( + return get_light_color() * vec3( get_diffuse() * diff_coeff * color + get_specular() * spec_coeff ); @@ -100,11 +112,10 @@ void main() { vec3 shaded_color = real_color.rgb; if(get_shading()){ - vec3 L = normalize(o_lightdir); + vec3 L = get_light_direction(); vec3 N = normalize(o_normal); - vec3 light1 = blinnphong(N, o_camdir, L, real_color.rgb); - vec3 light2 = blinnphong(N, o_camdir, -L, real_color.rgb); - shaded_color = get_ambient() * real_color.rgb + light1 + get_backlight() * light2; + vec3 light = blinnphong(N, normalize(o_camdir), L, real_color.rgb); + shaded_color = get_ambient() * real_color.rgb + light; } if (picking) { diff --git a/WGLMakie/assets/mesh.vert b/WGLMakie/assets/mesh.vert index 3b354497fd7..14341fbe452 100644 --- a/WGLMakie/assets/mesh.vert +++ b/WGLMakie/assets/mesh.vert @@ -1,12 +1,12 @@ out vec2 frag_uv; out vec3 o_normal; out vec3 o_camdir; -out vec3 o_lightdir; out vec4 frag_color; uniform mat4 projection; uniform mat4 view; +uniform vec3 eyeposition; vec3 tovec3(vec2 v){return vec3(v, 0.0);} vec3 tovec3(vec3 v){return v;} @@ -61,22 +61,15 @@ vec4 vertex_color(float value, vec2 colorrange, sampler2D colormap){ } } -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition) +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection) { // normal in world space o_normal = get_normalmatrix() * normal; - // position in view space (as seen from camera) - vec4 view_pos = view * position_world; // position in clip space (w/ depth) - gl_Position = projection * view_pos; + gl_Position = projection * view * position_world; // TODO consider using projectionview directly gl_Position.z += gl_Position.w * get_depth_shift(); - // direction to light - o_lightdir = normalize(view*vec4(lightposition, 1.0) - view_pos).xyz; // direction to camera - // This is equivalent to - // normalize(view*vec4(eyeposition, 1.0) - view_pos).xyz - // (by definition `view * eyeposition = 0`) - o_camdir = normalize(-view_pos).xyz; + o_camdir = position_world.xyz / position_world.w - eyeposition; } flat out uint frag_instance_id; @@ -90,7 +83,7 @@ void main(){ } vec4 position_world = model * vec4(vertex_position, 1); - render(position_world, get_normals(), view, projection, get_lightposition()); + render(position_world, get_normals(), view, projection); frag_uv = get_uv(); frag_uv = vec2(1.0 - frag_uv.y, frag_uv.x); frag_color = vertex_color(get_color(), get_colorrange(), colormap); diff --git a/WGLMakie/assets/particles.frag b/WGLMakie/assets/particles.frag index 5615fb356da..262a1fd9538 100644 --- a/WGLMakie/assets/particles.frag +++ b/WGLMakie/assets/particles.frag @@ -1,23 +1,34 @@ in vec4 frag_color; in vec3 frag_normal; in vec3 frag_position; -in vec3 frag_lightdir; +in vec3 o_camdir; + +// Smoothes out edge around 0 light intensity, see GLMakie +float smooth_zero_max(float x) { + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + float pow8 = x + shift; + pow8 = pow8 * pow8; pow8 = pow8 * pow8; pow8 = pow8 * pow8; + return x < yswap ? c * pow8 : x; +} vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0); + float backlight = get_backlight(); + float diff_coeff = smooth_zero_max(dot(L, -N)) + + backlight * smooth_zero_max(dot(L, N)); // specular coefficient - vec3 H = normalize(L+V); + vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0), 8.0); + float spec_coeff = pow(max(dot(H, -N), 0.0), get_shininess()) + + backlight * pow(max(dot(H, N), 0.0), get_shininess()); if (diff_coeff <= 0.0) spec_coeff = 0.0; // final lighting model - return vec3( - vec3(0.1) * vec3(0.3) + - vec3(0.9) * color * diff_coeff + - vec3(0.3) * spec_coeff + return get_light_color() * vec3( + get_diffuse() * diff_coeff * color + + get_specular() * spec_coeff ); } @@ -32,13 +43,12 @@ vec4 pack_int(uint id, uint index) { } void main() { - vec3 L, N, light1, light2, color; + vec3 L, N, light, color; if (get_shading()) { - L = normalize(frag_lightdir); + L = get_light_direction(); N = normalize(frag_normal); - light1 = blinnphong(N, frag_position, L, frag_color.rgb); - light2 = blinnphong(N, frag_position, -L, frag_color.rgb); - color = get_ambient() * frag_color.rgb + light1 + get_backlight() * light2; + light = blinnphong(N, normalize(o_camdir), L, frag_color.rgb); + color = get_ambient() * frag_color.rgb + light; } else { color = frag_color.rgb; } diff --git a/WGLMakie/assets/particles.vert b/WGLMakie/assets/particles.vert index f2785d2aed4..e9bd0a356c3 100644 --- a/WGLMakie/assets/particles.vert +++ b/WGLMakie/assets/particles.vert @@ -2,13 +2,12 @@ precision mediump float; uniform mat4 projection; uniform mat4 view; +uniform vec3 eyeposition; out vec3 frag_normal; out vec3 frag_position; - out vec4 frag_color; -out vec3 frag_lightdir; - +out vec3 o_camdir; vec3 qmul(vec4 q, vec3 v){ return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v); @@ -31,16 +30,14 @@ void main(){ // get_* gets the global inputs (uniform, sampler, position array) // those functions will get inserted by the shader creation pipeline vec3 vertex_position = get_markersize() * to_vec3(get_position()); - vec3 lightpos = vec3(20,20,20); vec3 N = get_normals(); rotate(get_rotations(), vertex_position, N); vertex_position = to_vec3(get_offset()) + vertex_position; vec4 position_world = model * vec4(vertex_position, 1); frag_normal = N; - frag_lightdir = normalize(lightpos - position_world.xyz); frag_color = to_vec4(get_color()); // direction to camera - frag_position = -position_world.xyz; + o_camdir = position_world.xyz / position_world.w - eyeposition; // screen space coordinates of the position gl_Position = projection * view * position_world; gl_Position.z += gl_Position.w * get_depth_shift(); diff --git a/WGLMakie/assets/sprites.vert b/WGLMakie/assets/sprites.vert index c077fa62446..35f8eaedd86 100644 --- a/WGLMakie/assets/sprites.vert +++ b/WGLMakie/assets/sprites.vert @@ -61,15 +61,19 @@ void main(){ vec2 sprite_bbox_centre = get_quad_offset() + bbox_signed_radius; mat4 pview = projection * view; - // Compute transform for the offset vectors from the central point mat4 trans = get_transform_marker() ? model : mat4(1.0); - trans = (get_billboard() ? projection : pview) * qmat(get_rotations()) * trans; // Compute centre of billboard in clipping coordinates - vec4 sprite_center = trans * vec4(sprite_bbox_centre, 0, 0); + // Always transform text/scatter position argument vec4 data_point = get_preprojection() * model * vec4(tovec3(get_pos()), 1); - data_point = vec4(data_point.xyz / data_point.w + mat3(model) * tovec3(get_marker_offset()), 1); + // maybe transform marker_offset + glyph offsets + data_point = vec4(data_point.xyz / data_point.w + mat3(trans) * tovec3(get_marker_offset()), 1); data_point = pview * data_point; + + // Compute transform for the offset vectors from the central point + trans = (get_billboard() ? projection : pview) * qmat(get_rotations()) * trans; + vec4 sprite_center = trans * vec4(sprite_bbox_centre, 0, 0); + vec4 vclip = data_point + sprite_center; // Extra buffering is required around sprites which are antialiased so that diff --git a/WGLMakie/assets/volume.frag b/WGLMakie/assets/volume.frag index ec37e457671..d3048f8afee 100644 --- a/WGLMakie/assets/volume.frag +++ b/WGLMakie/assets/volume.frag @@ -2,7 +2,6 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d bool _; //empty structs are not allowed }; in vec3 frag_vert; -in vec3 o_light_dir; const float max_distance = 1.3; @@ -54,16 +53,25 @@ vec3 gennormal(vec3 uvw, float d) return normalize(a-b); } +// Smoothes out edge around 0 light intensity, see GLMakie +float smooth_zero_max(float x) { + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + float pow8 = x + shift; + pow8 = pow8 * pow8; pow8 = pow8 * pow8; pow8 = pow8 * pow8; + return x < yswap ? c * pow8 : x; +} + vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0) + max(dot(L, -N), 0.0); + // TODO use backlight here too? + float diff_coeff = smooth_zero_max(dot(L, -N)) + smooth_zero_max(dot(L, N)); // specular coefficient vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0) + max(dot(H, -N), 0.0), shininess); + float spec_coeff = pow(max(dot(H, -N), 0.0) + max(dot(H, N), 0.0), shininess); // final lighting model - return vec3( - ambient * color + - diffuse * diff_coeff * color + - specular * spec_coeff + return ambient * color + get_light_color() * vec3( + get_diffuse() * diff_coeff * color + + get_specular() * spec_coeff ); } @@ -122,14 +130,14 @@ vec4 contours(vec3 front, vec3 dir) float T = 1.0; vec3 Lo = vec3(0.0); int i = 0; - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); for (i; i < num_samples; ++i) { float intensity = texture(volumedata, pos).x; vec4 density = color_lookup(intensity, colormap, colorrange); float opacity = density.a; if(opacity > 0.0){ vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); + vec3 L = get_light_direction(); vec3 opaque = blinnphong(N, camdir, L, density.rgb); Lo += (T * opacity) * opaque; T *= 1.0 - opacity; @@ -147,12 +155,12 @@ vec4 isosurface(vec3 front, vec3 dir) vec4 c = vec4(0.0); int i = 0; vec4 diffuse_color = color_lookup(isovalue, colormap, colorrange); - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); for (i; i < num_samples; ++i){ float density = texture(volumedata, pos).x; if(abs(density - isovalue) < isorange){ vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); + vec3 L = get_light_direction(); c = vec4( blinnphong(N, camdir, L, diffuse_color.rgb), diffuse_color.a diff --git a/WGLMakie/assets/volume.vert b/WGLMakie/assets/volume.vert index 42599e3e6a3..c9d00be85b8 100644 --- a/WGLMakie/assets/volume.vert +++ b/WGLMakie/assets/volume.vert @@ -1,5 +1,4 @@ out vec3 frag_vert; -out vec3 o_light_dir; uniform mat4 projection, view; @@ -7,7 +6,6 @@ void main() { frag_vert = position; vec4 world_vert = model * vec4(position, 1); - o_light_dir = vec3(modelinv * vec4(get_lightposition(), 1)); gl_Position = projection * view * world_vert; gl_Position.z += gl_Position.w * get_depth_shift(); } diff --git a/WGLMakie/src/Camera.js b/WGLMakie/src/Camera.js index 86de8616b19..c63ef596bbd 100644 --- a/WGLMakie/src/Camera.js +++ b/WGLMakie/src/Camera.js @@ -39,7 +39,7 @@ function in_scene(scene, mouse_event) { } // Taken from https://andreasrohner.at/posts/Web%20Development/JavaScript/Simple-orbital-camera-controls-for-THREE-js/ -export function attach_3d_camera(canvas, makie_camera, cam3d, scene) { +export function attach_3d_camera(canvas, makie_camera, cam3d, light_dir, scene) { if (cam3d === undefined) { // we just support 3d cameras atm return; @@ -77,6 +77,8 @@ export function attach_3d_camera(canvas, makie_camera, cam3d, scene) { [width, height], [x, y, z] ); + + makie_camera.update_light_dir(light_dir.value); } function addMouseHandler(domObject, drag, zoomIn, zoomOut) { let startDragX = null; @@ -254,6 +256,10 @@ export class MakieCamera { // Lazy calculation, only if a plot type requests them // will be of the form: {[space, markerspace]: THREE.Uniform(...)} this.preprojections = {}; + + // For camera-relative light directions + // TODO: intial position wrong... + this.light_direction = new THREE.Uniform(new THREE.Vector3(-1, -1, -1).normalize()); } calculate_matrices() { @@ -288,6 +294,13 @@ export class MakieCamera { return; } + update_light_dir(light_dir) { + const T = new THREE.Matrix3().setFromMatrix4(this.view.value).invert(); + const new_dir = new THREE.Vector3().fromArray(light_dir); + new_dir.applyMatrix3(T).normalize(); + this.light_direction.value = new_dir; + } + clip_to_space(space) { if (space === "data") { return this.projectionview_inverse.value; diff --git a/WGLMakie/src/Serialization.js b/WGLMakie/src/Serialization.js index 9b668190d1f..486bf13842b 100644 --- a/WGLMakie/src/Serialization.js +++ b/WGLMakie/src/Serialization.js @@ -209,6 +209,21 @@ export function add_plot(scene, plot_data) { markerspace.value ); } + + if (scene.camera_relative_light) { + plot_data.uniforms.light_direction = cam.light_direction; + scene.light_direction.on(value => { + cam.update_light_dir(value); + }) + } else { + // TODO how update? + const light_dir = new THREE.Vector3().fromArray(scene.light_direction.value); + plot_data.uniforms.light_direction = new THREE.Uniform(light_dir); + scene.light_direction.on(value => { + plot_data.uniforms.light_direction.value.fromArray(value); + }) + } + const p = deserialize_plot(plot_data); plot_cache[p.plot_uuid] = p; scene.add(p); @@ -534,6 +549,8 @@ export function deserialize_scene(data, screen) { scene.backgroundcolor = data.backgroundcolor; scene.clearscene = data.clearscene; scene.visible = data.visible; + scene.camera_relative_light = data.camera_relative_light; + scene.light_direction = data.light_direction; const camera = new Camera.MakieCamera(); @@ -545,11 +562,12 @@ export function deserialize_scene(data, screen) { } update_cam(data.camera.value); + camera.update_light_dir(data.light_direction.value); if (data.cam3d_state) { - // add JS camera... This will only update the camera matrices via js if: + // add JS camera... This will only update the camera matrices via js if: // JSServe.can_send_to_julia && can_send_to_julia() - Camera.attach_3d_camera(canvas, camera, data.cam3d_state, scene); + Camera.attach_3d_camera(canvas, camera, data.cam3d_state, data.light_direction, scene); } data.camera.on(update_cam); data.plots.forEach((plot_data) => { diff --git a/WGLMakie/src/imagelike.jl b/WGLMakie/src/imagelike.jl index d77184312b2..4eee9356461 100644 --- a/WGLMakie/src/imagelike.jl +++ b/WGLMakie/src/imagelike.jl @@ -11,11 +11,16 @@ function create_shader(mscene::Scene, plot::Surface) px, py, pz = plot[1], plot[2], plot[3] grid(x, y, z, trans, space) = Makie.matrix_grid(p-> apply_transform(trans, p, space), x, y, z) - positions = Buffer(lift(grid, plot, px, py, pz, transform_func_obs(plot), get(plot, :space, :data))) - rect = lift(z -> Tesselation(Rect2(0f0, 0f0, 1f0, 1f0), size(z)), pz) - faces = Buffer(lift(r -> decompose(GLTriangleFace, r), plot, rect)) + # TODO: Use Makie.surface2mesh + ps = lift(grid, plot, px, py, pz, transform_func_obs(plot), get(plot, :space, :data)) + positions = Buffer(ps) + rect = lift(z -> Tesselation(Rect2(0f0, 0f0, 1f0, 1f0), size(z)), plot, pz) + fs = lift(r -> decompose(QuadFace{Int}, r), plot, rect) + fs = map((ps, fs) -> filter(f -> !any(i -> isnan(ps[i]), f), fs), plot, ps, fs) + faces = Buffer(fs) + # Why does this need to be different from surface2mesh? uv = Buffer(lift(decompose_uv, plot, rect)) - normals = Buffer(lift(surface_normals, plot, px, py, pz)) + normals = Buffer(lift(Makie.nan_aware_normals, plot, ps, fs)) per_vertex = Dict(:positions => positions, :faces => faces, :uv => uv, :normals => normals) uniforms = Dict(:uniform_color => color, :color => false) @@ -70,7 +75,8 @@ function create_shader(mscene::Scene, plot::Volume) :depth_shift => get(plot, :depth_shift, Observable(0.0f0)), # these get filled in later by serialization, but we need them # as dummy values here, so that the correct uniforms are emitted - :lightposition => Vec3f(1), + :light_direction => Vec3f(1), + :light_color => Vec3f(1), :eyeposition => Vec3f(1), :ambient => Vec3f(1), :picking => false, diff --git a/WGLMakie/src/meshes.jl b/WGLMakie/src/meshes.jl index e1201f49e7f..ded9dee2ab7 100644 --- a/WGLMakie/src/meshes.jl +++ b/WGLMakie/src/meshes.jl @@ -64,16 +64,17 @@ function draw_mesh(mscene::Scene, per_vertex, plot, uniforms; permute_tex=true) get!(uniforms, :pattern, false) get!(uniforms, :model, plot.model) - get!(uniforms, :lightposition, Vec3f(1)) get!(uniforms, :ambient, Vec3f(1)) + get!(uniforms, :light_direction, Vec3f(1)) + get!(uniforms, :light_color, Vec3f(1)) uniforms[:interpolate_in_fragment_shader] = get(plot, :interpolate_in_fragment_shader, true) - get!(uniforms, :shading, get(plot, :shading, false)) + get!(uniforms, :shading, to_value(get(plot, :shading, NoShading)) != NoShading) - uniforms[:normalmatrix] = map(mscene.camera.view, plot.model) do v, m + uniforms[:normalmatrix] = map(plot.model) do m i = Vec(1, 2, 3) - return transpose(inv(v[i, i] * m[i, i])) + return transpose(inv(m[i, i])) end diff --git a/WGLMakie/src/particles.jl b/WGLMakie/src/particles.jl index 7bdd7703b72..5c4a3bc681f 100644 --- a/WGLMakie/src/particles.jl +++ b/WGLMakie/src/particles.jl @@ -38,7 +38,8 @@ end const IGNORE_KEYS = Set([ :shading, :overdraw, :rotation, :distancefield, :space, :markerspace, :fxaa, :visible, :transformation, :alpha, :linewidth, :transparency, :marker, - :lightposition, :cycle, :label, :inspector_clear, :inspector_hover, + :light_direction, :light_color, + :cycle, :label, :inspector_clear, :inspector_hover, :inspector_label, :axis_cycler ]) @@ -77,13 +78,19 @@ function create_shader(scene::Scene, plot::MeshScatter) uniform_dict[:depth_shift] = get(plot, :depth_shift, Observable(0f0)) uniform_dict[:backlight] = plot.backlight - get!(uniform_dict, :ambient, Vec3f(1)) + # Make sure these exist + get!(uniform_dict, :ambient, Vec3f(0.1)) + get!(uniform_dict, :diffuse, Vec3f(0.9)) + get!(uniform_dict, :specular, Vec3f(0.3)) + get!(uniform_dict, :shininess, 8f0) + get!(uniform_dict, :light_direction, Vec3f(1)) + get!(uniform_dict, :light_color, Vec3f(1)) # id + picking gets filled in JS, needs to be here to emit the correct shader uniforms uniform_dict[:picking] = false uniform_dict[:object_id] = UInt32(0) - uniform_dict[:shading] = plot.shading + uniform_dict[:shading] = map(x -> x != NoShading, plot.shading) return InstancedProgram(WebGL(), lasset("particles.vert"), lasset("particles.frag"), instance, VertexArray(; per_instance...), uniform_dict) diff --git a/WGLMakie/src/serialization.jl b/WGLMakie/src/serialization.jl index 844e80147fe..f96870589fa 100644 --- a/WGLMakie/src/serialization.jl +++ b/WGLMakie/src/serialization.jl @@ -292,10 +292,16 @@ function serialize_scene(scene::Scene) children = map(child-> serialize_scene(child), scene.children) + dirlight = Makie.get_directional_light(scene) + light_dir = isnothing(dirlight) ? Observable(Vec3f(1)) : dirlight.direction + cam_rel = isnothing(dirlight) ? false : dirlight.camera_relative + serialized = Dict(:pixelarea => pixel_area, :backgroundcolor => lift(hexcolor, scene, scene.backgroundcolor), :clearscene => scene.clear, :camera => serialize_camera(scene), + :light_direction => light_dir, + :camera_relative_light => cam_rel, :plots => serialize_plots(scene, scene.plots), :cam3d_state => cam3d_state, :visible => scene.visible, @@ -331,11 +337,11 @@ function serialize_three(scene::Scene, @nospecialize(plot::AbstractPlot)) uniforms = mesh[:uniforms] updater = mesh[:uniform_updater] - pointlight = Makie.get_point_light(scene) - if !isnothing(pointlight) - uniforms[:lightposition] = serialize_three(pointlight.position[]) - on(plot, pointlight.position) do value - updater[] = [:lightposition, serialize_three(value)] + dirlight = Makie.get_directional_light(scene) + if !isnothing(dirlight) + uniforms[:light_color] = serialize_three(dirlight.color[]) + on(plot, dirlight.color) do value + updater[] = [:light_color, serialize_three(value)] return end end diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index fc6e4daa184..e711f155bbe 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -20276,7 +20276,7 @@ function in_scene(scene, mouse_event) { const [sx, sy, sw, sh] = scene.pixelarea.value; return x >= sx && x < sx + sw && y >= sy && y < sy + sh; } -function attach_3d_camera(canvas, makie_camera, cam3d, scene) { +function attach_3d_camera(canvas, makie_camera, cam3d, light_dir, scene) { if (cam3d === undefined) { return; } @@ -20306,6 +20306,7 @@ function attach_3d_camera(canvas, makie_camera, cam3d, scene) { y, z ]); + makie_camera.update_light_dir(light_dir.value); } function addMouseHandler(domObject, drag, zoomIn, zoomOut) { let startDragX = null; @@ -20472,6 +20473,7 @@ class MakieCamera { this.resolution = new Pu(new Z()); this.eyeposition = new Pu(new A()); this.preprojections = {}; + this.light_direction = new Pu(new A(-1, -1, -1).normalize()); } calculate_matrices() { const [w, h] = this.resolution.value; @@ -20494,6 +20496,12 @@ class MakieCamera { this.calculate_matrices(); return; } + update_light_dir(light_dir) { + const T = new He().setFromMatrix4(this.view.value).invert(); + const new_dir = new A().fromArray(light_dir); + new_dir.applyMatrix3(T).normalize(); + this.light_direction.value = new_dir; + } clip_to_space(space) { if (space === "data") { return this.projectionview_inverse.value; @@ -20943,6 +20951,18 @@ function add_plot(scene, plot_data) { const { space , markerspace } = plot_data; plot_data.uniforms.preprojection = cam.preprojection_matrix(space.value, markerspace.value); } + if (scene.camera_relative_light) { + plot_data.uniforms.light_direction = cam.light_direction; + scene.light_direction.on((value)=>{ + cam.update_light_dir(value); + }); + } else { + const light_dir = new mod.Vector3().fromArray(scene.light_direction.value); + plot_data.uniforms.light_direction = new mod.Uniform(light_dir); + scene.light_direction.on((value)=>{ + plot_data.uniforms.light_direction.value.fromArray(value); + }); + } const p = deserialize_plot(plot_data); plot_cache[p.plot_uuid] = p; scene.add(p); @@ -21213,6 +21233,8 @@ function deserialize_scene(data, screen) { scene.backgroundcolor = data.backgroundcolor; scene.clearscene = data.clearscene; scene.visible = data.visible; + scene.camera_relative_light = data.camera_relative_light; + scene.light_direction = data.light_direction; const camera = new MakieCamera(); scene.wgl_camera = camera; function update_cam(camera_matrices) { @@ -21220,8 +21242,9 @@ function deserialize_scene(data, screen) { camera.update_matrices(view, projection, resolution, eyepos); } update_cam(data.camera.value); + camera.update_light_dir(data.light_direction.value); if (data.cam3d_state) { - attach_3d_camera(canvas, camera, data.cam3d_state, scene); + attach_3d_camera(canvas, camera, data.cam3d_state, data.light_direction, scene); } data.camera.on(update_cam); data.plots.forEach((plot_data)=>{ diff --git a/docs/explanations/lighting.md b/docs/explanations/lighting.md deleted file mode 100644 index 9ba0573a8d7..00000000000 --- a/docs/explanations/lighting.md +++ /dev/null @@ -1,74 +0,0 @@ -# Lighting - -For 3D scenes, `GLMakie` offers several attributes to control the lighting of the material. - -- `ambient::Vec3f`: Objects should never be completely dark; we use an ambient light to simulate background lighting, and give the object some color. Each element of the vector represents the intensity of color in R, G or B respectively. -- `diffuse::Vec3f`: Simulates the directional impact which the light source has on the plot object. This is the most visually significant component of the lighting model; the more a part of an object faces the light source, the brighter it becomes. Each element of the vector represents the intensity of color in R, G or B respectively. -- `specular::Vec3f`: Simulates the bright spot of a light that appears on shiny objects. Specular highlights are more inclined to the color of the light than the color of the object. Each element of the vector represents the intensity of color in R, G or B respectively. -- `shininess::Float32`: Controls the shininess of the object. Higher shininess reduces the size of the highlight, and makes it sharper. This value must be positive. -- `lightposition::Vec3f`: The location of the main light source; by default, the light source is at the location of the camera. - -You can find more information on how these were implemented [here](https://learnopengl.com/Lighting/Basic-Lighting). -Some usage examples can be found in the [RPRMakie examples](https://docs.makie.org/stable/documentation/backends/rprmakie/) and in the [examples](https://docs.makie.org/stable/documentation/lighting/#examples). - -## SSAO - -GLMakie also implements [_screen-space ambient occlusion_](https://learnopengl.com/Advanced-Lighting/SSAO), which is an algorithm to more accurately simulate the scattering of light. There are a couple of controllable scene attributes nested within the `SSAO` toplevel attribute: - -- `radius` sets the range of SSAO. You may want to scale this up or - down depending on the limits of your coordinate system -- `bias` sets the minimum difference in depth required for a pixel to - be occluded. Increasing this will typically make the occlusion - effect stronger. -- `blur` sets the (pixel) range of the blur applied to the occlusion texture. - The texture contains a (random) pattern, which is washed out by - blurring. Small `blur` will be faster, sharper and more patterned. - Large `blur` will be slower and smoother. Typically `blur = 2` is - a good compromise. - -!!! note - The SSAO postprocessor is turned off by default to save on resources. To turn it on, set `GLMakie.activate!(ssao=true)`, close any existing GLMakie window and reopen it. - -## Matcap - -A matcap (material capture) is a texture which is applied based on the normals of a given mesh. They typically include complex materials and lighting and offer a cheap way to apply those to any mesh. You may pass a matcap via the `matcap` attribute of a `mesh`, `meshscatter` or `surface` plot. Setting `shading = false` is suggested. You can find a lot matcaps [here](https://github.com/nidorx/matcaps). - -## Examples - - - -\begin{examplefigure}{} -```julia -using GLMakie -GLMakie.activate!(ssao=true) -GLMakie.closeall() # close any open screen - -fig = Figure() -ssao = Makie.SSAO(radius = 5.0, blur = 3) -ax = LScene(fig[1, 1], scenekw = (ssao=ssao,)) -# SSAO attributes are per scene -ax.scene.ssao.bias[] = 0.025 - -box = Rect3(Point3f(-0.5), Vec3f(1)) -positions = [Point3f(x, y, rand()) for x in -5:5 for y in -5:5] -meshscatter!(ax, positions, marker=box, markersize=1, color=:lightblue, ssao=true) -fig -``` -\end{examplefigure} - -```julia:disable-ssao -GLMakie.activate!(ssao=false) # hide -GLMakie.closeall() # hide -``` - -\begin{examplefigure}{} -```julia -using FileIO -using GLMakie -GLMakie.activate!() # hide -catmesh = FileIO.load(assetpath("cat.obj")) -gold = FileIO.load(download("https://raw.githubusercontent.com/nidorx/matcaps/master/1024/E6BF3C_5A4719_977726_FCFC82.png")) - -mesh(catmesh, matcap=gold, shading=false) -``` -\end{examplefigure} diff --git a/docs/explanations/transparency.md b/docs/explanations/transparency.md index 6347938c2c7..bbe975e302b 100644 --- a/docs/explanations/transparency.md +++ b/docs/explanations/transparency.md @@ -66,8 +66,8 @@ CairoMakie.activate!() # hide fig = Figure() ax = LScene(fig[1, 1], show_axis=false) -p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = false) -p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = false) +p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = NoShading) +p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = NoShading) rotate!(p1, Vec3f(0, 1, 0), 0.1) rotate!(p2, Vec3f(0, 1, 0), -0.1) fig @@ -81,8 +81,8 @@ GLMakie.activate!() # hide fig = Figure() ax = LScene(fig[1, 1], show_axis=false) -p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = false) -p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = false) +p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = NoShading) +p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = NoShading) rotate!(p1, Vec3f(0, 1, 0), 0.1) rotate!(p2, Vec3f(0, 1, 0), -0.1) fig @@ -105,9 +105,9 @@ GLMakie.activate!() # hide fig = Figure() ax = LScene(fig[1, 1], show_axis=false) -p1 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = false, transparency = true) -p2 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:blue, 0.5), shading = false, transparency = true) -p3 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = false, transparency = true) +p1 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = NoShading, transparency = true) +p2 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:blue, 0.5), shading = NoShading, transparency = true) +p3 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = NoShading, transparency = true) for (dz, p) in zip((-1, 0, 1), (p1, p2, p3)) translate!(p, 0, 0, dz) end @@ -126,16 +126,16 @@ 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) +p1 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :red, shading = NoShading, transparency = true) +p2 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :blue, shading = NoShading, transparency = true) +p3 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :red, shading = NoShading, transparency = true) for (dz, p) in zip((-1, 0, 1), (p1, p2, p3)) translate!(p, 0, 0, dz) end ax2 = LScene(fig[1, 2], show_axis=false) -p1 = mesh!(ax2, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = false, transparency=true) -p2 = mesh!(ax2, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = false, transparency=true) +p1 = mesh!(ax2, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = NoShading, transparency=true) +p2 = mesh!(ax2, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = NoShading, transparency=true) rotate!(p1, Vec3f(0, 1, 0), 0.1) rotate!(p2, Vec3f(0, 1, 0), -0.1) fig diff --git a/docs/reference/blocks/polaraxis.md b/docs/reference/blocks/polaraxis.md index 1ca6d919833..ace9fca11fd 100644 --- a/docs/reference/blocks/polaraxis.md +++ b/docs/reference/blocks/polaraxis.md @@ -104,7 +104,7 @@ ax = PolarAxis(f[1, 1], title = "Surface") rs = 0:10 phis = range(0, 2pi, 37) cs = [r+cos(4phi) for phi in phis, r in rs] -p = surface!(ax, 0..2pi, 0..10, cs, shading = false, colormap = :coolwarm) +p = surface!(ax, 0..2pi, 0..10, cs, shading = NoShading, colormap = :coolwarm) ax.gridz[] = 100 tightlimits!(ax) # surface plots include padding by default Colorbar(f[2, 1], p, vertical = false, flipaxis = false) diff --git a/docs/reference/plots/mesh.md b/docs/reference/plots/mesh.md index 9f7941e0cc4..30653df5d09 100644 --- a/docs/reference/plots/mesh.md +++ b/docs/reference/plots/mesh.md @@ -24,7 +24,7 @@ faces = [ colors = [:red, :green, :blue, :orange] -scene = mesh(vertices, faces, color = colors, shading = false) +scene = mesh(vertices, faces, color = colors, shading = NoShading) ``` \end{examplefigure} diff --git a/docs/reference/plots/surface.md b/docs/reference/plots/surface.md index ffaa7f07926..04a702c0e99 100644 --- a/docs/reference/plots/surface.md +++ b/docs/reference/plots/surface.md @@ -79,7 +79,7 @@ data = 0.1randn(d,d) + reshape( d, d ) -surface(data; shading=false, colormap = :deep) -surface(data; shading=false, colormap = :deep) +surface(data; shading = NoShading, colormap = :deep) +surface(data; shading = NoShading, colormap = :deep) ``` \end{examplefigure} diff --git a/docs/reference/scene.md b/docs/reference/scene.md new file mode 100644 index 00000000000..fe7252d7605 --- /dev/null +++ b/docs/reference/scene.md @@ -0,0 +1,5 @@ +# Scene + +{{list_folder_with_images scene}} + +See also: [Scene page](https://docs.makie.org/stable/explanations/scenes/) \ No newline at end of file diff --git a/docs/reference/scene/SSAO.md b/docs/reference/scene/SSAO.md new file mode 100644 index 00000000000..653789af1a5 --- /dev/null +++ b/docs/reference/scene/SSAO.md @@ -0,0 +1,43 @@ +# SSAO + +GLMakie also implements [_screen-space ambient occlusion_](https://learnopengl.com/Advanced-Lighting/SSAO), which is an algorithm to more accurately simulate the scattering of light. There are a couple of controllable scene attributes nested within the `SSAO` toplevel attribute: + +- `radius` sets the range of SSAO. You may want to scale this up or + down depending on the limits of your coordinate system +- `bias` sets the minimum difference in depth required for a pixel to + be occluded. Increasing this will typically make the occlusion + effect stronger. +- `blur` sets the (pixel) range of the blur applied to the occlusion texture. + The texture contains a (random) pattern, which is washed out by + blurring. Small `blur` will be faster, sharper and more patterned. + Large `blur` will be slower and smoother. Typically `blur = 2` is + a good compromise. + +!!! note + The SSAO postprocessor is turned off by default to save on resources. To turn it on, set `GLMakie.activate!(ssao=true)`, close any existing GLMakie window and reopen it. + +## Example + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!(ssao=true) +GLMakie.closeall() # close any open screen + +fig = Figure() +ssao = Makie.SSAO(radius = 5.0, blur = 3) +ax = LScene(fig[1, 1], scenekw = (ssao=ssao,)) +# SSAO attributes are per scene +ax.scene.ssao.bias[] = 0.025 + +box = Rect3(Point3f(-0.5), Vec3f(1)) +positions = [Point3f(x, y, rand()) for x in -5:5 for y in -5:5] +meshscatter!(ax, positions, marker=box, markersize=1, color=:lightblue, ssao=true) +fig +``` +\end{examplefigure} + +```julia:disable-ssao +GLMakie.activate!(ssao=false) # hide +GLMakie.closeall() # hide +``` diff --git a/docs/reference/scene/lighting.md b/docs/reference/scene/lighting.md new file mode 100644 index 00000000000..03babd0c7ca --- /dev/null +++ b/docs/reference/scene/lighting.md @@ -0,0 +1,205 @@ +# Lighting + +The Lighting capabilities of Makie differ between backends and plot types. +They are implemented for mesh related plot types (`mesh`, `meshscatter`, `surface`), their derivatives (e.g. 3D `arrows`) and to some degree `volume` plots (and `contour3d`). +With respect to Backends: + +- GLMakie implements the baseline lighting model and will act as our default for this page. +- WGLMakie implements a simplified version of GLMakie's lighting. +- CairoMakie implements limited lighting due to its limited 3D capabilities +- RPRMakie implements parts of Makies lighting model but can also use more sophisticated methods from RadeonProRender. + +## Material Attributes + +In 3D rendering a material describes how an object reacts to light. +This can include the color of an object, how bright and sharp specular reflections are, how metallic it looks, how rough it is and more. +In Makie however the model is still fairly simple and limited. +Currently the following material attributes are available: +- `diffuse::Vec3f = Vec3f(1.0)`: controls how strong the diffuse reflections of an object are in the red, green and blue color channel. A diffuse reflection is one where incoming light is scattered in every direction. The strength of this reflection is based on the amount of light hitting the surface, which is proportional to `dot(light_direction, -normal)`. It generally makes up the main color of an object in light. +- `specular::Vec3f = Vec3f(0.4)`: controls the strength of specular reflection in the red, green and blue color channels. A specular reflection is a direct reflection of light, i.e. one where the incoming angle `dot(light_direction, -normal)` matches the outgoing angle `dot(camera_direction, -normal)`. It responsible for bright spots on objects. Note that this does not take the color of the object into account, as specular reflections typically match the light color. +- `shininess::Float32 = 32f0`: controls how sharp specular reflections are. Low shininess will allow a larger difference between incoming outgoing angle to take effect, creating a larger and smoother bright spot. High shininess will respectively reduce the size of the bright spot and increase its sharpness. This value must be positive. +- `backlight::Float32 = 0f0` controls how strongly light interacts with the backside of an object. Setting this to a value `> 0` can be helpful when visualizing a surface. (More precisely the light calculation is repeated with inverted normals and the result is mixed in with `backlight` as a prefactor.) + +!!! note + RPRMakie does not use these material attributes. + Instead it relies on RadeonProRender's material system, which is passed through the `material` attribute. + See the [RPRMakie page](https://docs.makie.org/stable/documentation/backends/rprmakie/) for examples. + + +## Lighting alogrithm + +Lights are controlled through the `lights` vector in a `scene` and by the `shading` attribute in a plot. +Generally you will not need to set `shading` yourself, as it is derived based on the lights vector. +The possible options for `shading` are: +- `shading = NoShading` disables light calculations, resulting in the plain color of an object being shown. +- `shading = FastShading` enables a simplified lighting model which only allows for one `AmbientLight` and one `DirectionalLight`. +- `shading = MultiLightShading` is a GLMakie exclusive option which enables multiple light sources (as set in the `ScreenConfig`, default up to 64) as well as `PointLight` and `SpotLight`. + +!!! note + You can access the underlying scene of an `Axis3` with `ax.scene`. + +For reference all the lighting calculations (except ambient) in GLMakie, WGLMakie and to some extend CairoMakie end up using the [Blinn-Phong reflection model](https://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_reflection_model) which boils down to + +```julia +function blinn_phong( + diffuse, specular, shininess, normal, object_color, + light_color, light_direction, camera_direction + ) + diffuse_coefficient = max(dot(light_direction, -normal), 0.0) + H = normalize(light_direction + camera_direction) + specular_coefficient = max(dot(H, -normal), 0.0)^shininess + return light_color * ( + diffuse * diffuse_coefficient * object_color + + specular * specular_coefficient + ) +end +``` + +The different light sources control the `light_direction` and may further adjust the result of this function. For example, `SpotLight` adds a factor which reduces light intensity outside its area. + + +## Types of Light + + +### AmbientLight + +{{doc AmbientLight}} + +\begin{examplefigure}{} +```julia +using CairoMakie +CairoMakie.activate!() # hide + +fig = Figure(resolution = (600, 600)) +ax11 = LScene(fig[1, 1], scenekw = (lights = [],)) +ax12 = LScene(fig[1, 2], scenekw = (lights = [AmbientLight(RGBf(0, 0, 0))],)) +ax21 = LScene(fig[2, 1], scenekw = (lights = [AmbientLight(RGBf(0.7, 0.7, 0.7))],)) +ax22 = LScene(fig[2, 2], scenekw = (lights = [AmbientLight(RGBf(0.8, 0.3, 0))],)) +for ax in (ax11, ax12, ax21, ax22) + mesh!(ax, Sphere(Point3f(0), 1f0), color = :white) +end +fig +``` +\end{examplefigure} + + +### DirectionalLight + +{{doc DirectionalLight}} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +fig = Figure(resolution = (600, 600)) +ax11 = LScene(fig[1, 1], scenekw = (lights = [DirectionalLight(RGBf(0, 0, 0), Vec3f(-1, 0, 0))],)) +ax12 = LScene(fig[1, 2], scenekw = (lights = [DirectionalLight(RGBf(1, 1, 1), Vec3f(-1, 0, 0))],)) +lights = [ + DirectionalLight(RGBf(0, 0, 0.7), Vec3f(-1, -1, 0)), + DirectionalLight(RGBf(0.7, 0.2, 0), Vec3f(-1, 1, -1)), + DirectionalLight(RGBf(0.7, 0.7, 0.7), Vec3f(1, -1, -1)) +] +ax21 = LScene(fig[2, 1], scenekw = (lights = lights,)) +ax22 = LScene(fig[2, 2], scenekw = (lights = [DirectionalLight(RGBf(4, 2, 1), Vec3f(0, 0, -1))],)) +for ax in (ax11, ax12, ax21, ax22) + mesh!(ax, Sphere(Point3f(0), 1f0), color = :white) +end +fig +``` +\end{examplefigure} + +### PointLight + +{{doc PointLight}} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +fig = Figure(resolution = (600, 600)) +ax = LScene(fig[1, 1], scenekw = (lights = [PointLight(RGBf(1, 1, 1), Point3f(0, 0, 0))],)) +ps = [Point3f(x, y, z) for x in (-1, 0, 1) for y in (-1, 0, 1) for z in (-1, 0, 1)] +meshscatter!(ax, ps, color = :white) +fig +``` +\end{examplefigure} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +lights = [ + PointLight(RGBf(1, 1, 1), Point3f(0, 0, 5), 50), + PointLight(RGBf(2, 0, 0), Point3f(-3, -3, 2), 10), + PointLight(RGBf(0, 2, 0), Point3f(-3, 3, 2), 10), + PointLight(RGBf(0, 0, 2), Point3f( 3, 3, 2), 10), + PointLight(RGBf(2, 2, 0), Point3f( 3, -3, 2), 10), +] + +fig = Figure(resolution = (600, 600)) +ax = LScene(fig[1, 1], scenekw = (lights = lights,)) +ps = [Point3f(x, y, 0) for x in -5:5 for y in -5:5] +meshscatter!(ax, ps, color = :white, markersize = 0.75) +scatter!(ax, map(l -> l.position[], lights), color = map(l -> l.color[], lights), strokewidth = 1, strokecolor = :black) +fig +``` +\end{examplefigure} + +With a strong PointLight and Attenuation you can create different colors at different distances. + +\begin{examplefigure}{} +```julia +using GLMakie, GeometryBasics +GLMakie.activate!() # hide + +ps = [ + Point3f(cosd(phi) * cosd(theta), sind(phi) * cosd(theta), sind(theta)) + for theta in range(-20, 20, length = 21) for phi in range(60, 340, length=30) +] +faces = [QuadFace(30j + i, 30j + mod1(i+1, 30), 30*(j+1) + mod1(i+1, 30), 30*(j+1) + i) for j in 0:19 for i in 1:29] +m = GeometryBasics.Mesh(meta(ps, normals = ps), decompose(GLTriangleFace, faces)) + +lights = [PointLight(RGBf(10, 4, 2), Point3f(0, 0, 0), 5)] + +fig = Figure(resolution = (600, 600), backgroundcolor = :black) +ax = LScene(fig[1, 1], scenekw = (lights = lights,), show_axis = false) +update_cam!(ax.scene, ax.scene.camera_controls, Rect3f(Point3f(-2), Vec3f(4))) +meshscatter!( + ax, [Point3f(0) for _ in 1:14], marker = m, markersize = 0.1:0.2:3.0, + color = :white, backlight = 1f0, transparency = false) +fig +``` +\end{examplefigure} + + +### SpotLight + +{{doc SpotLight}} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide +GLMakie.closeall() # hide + +lights = [ + SpotLight(RGBf(1, 0, 0), Point3f(-3, 0, 3), Vec3f(0, 0, -1), Vec2f(0.0, 0.3pi)), + SpotLight(RGBf(0, 1, 0), Point3f( 0, 3, 3), Vec3f(0, -0.5, -1), Vec2f(0.2pi, 0.25pi)), + SpotLight(RGBf(0, 0, 1), Point3f( 3, 0, 3), Vec3f(0, 0, -1), Vec2f(0.25pi, 0.25pi)), +] + +fig = Figure(resolution = (600, 600)) +ax = LScene(fig[1, 1], scenekw = (lights = lights,)) +ps = [Point3f(x, y, 0) for x in -5:5 for y in -5:5] +meshscatter!(ax, ps, color = :white, markersize = 0.75) +scatter!(ax, map(l -> l.position[], lights), color = map(l -> l.color[], lights), strokewidth = 1, strokecolor = :black) +fig +``` +\end{examplefigure} + +### EnvironmentLight + +{{doc EnvironmentLight}} \ No newline at end of file diff --git a/docs/reference/scene/matcap.md b/docs/reference/scene/matcap.md new file mode 100644 index 00000000000..a9b320dc29b --- /dev/null +++ b/docs/reference/scene/matcap.md @@ -0,0 +1,17 @@ +# Matcap + +A matcap (material capture) is a texture which is applied based on the normals of a given mesh. They typically include complex materials and lighting and offer a cheap way to apply those to any mesh. You may pass a matcap via the `matcap` attribute of a `mesh`, `meshscatter` or `surface` plot. Setting `shading = NoShading` is suggested. You can find a lot matcaps [here](https://github.com/nidorx/matcaps). + +## Example + +\begin{examplefigure}{} +```julia +using FileIO +using GLMakie +GLMakie.activate!() # hide +catmesh = FileIO.load(assetpath("cat.obj")) +gold = FileIO.load(download("https://raw.githubusercontent.com/nidorx/matcaps/master/1024/E6BF3C_5A4719_977726_FCFC82.png")) + +mesh(catmesh, matcap=gold, shading = NoShading) +``` +\end{examplefigure} \ No newline at end of file diff --git a/src/Makie.jl b/src/Makie.jl index 52ad940bda3..727b7619b59 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -83,6 +83,7 @@ using MakieCore: Arrows, Heatmap, Image, Lines, LineSegments, Mesh, MeshScatter, using MakieCore: ConversionTrait, NoConversion, PointBased, GridBased, VertexBasedGrid, CellBasedGrid, ImageLike, VolumeLike using MakieCore: Key, @key_str, Automatic, automatic, @recipe using MakieCore: Pixel, px, Unit, Billboard +using MakieCore: NoShading, FastShading, MultiLightShading using MakieCore: not_implemented_for import MakieCore: plot, plot!, theme, plotfunc, plottype, merge_attributes!, calculated_attributes!, get_attribute, plotsym, plotkey, attributes, used_attributes @@ -114,6 +115,7 @@ include("interaction/liftmacro.jl") include("colorsampler.jl") include("patterns.jl") include("utilities/utilities.jl") # need Makie.AbstractPattern +include("lighting.jl") # Basic scene/plot/recipe interfaces + types include("scenes.jl") @@ -269,6 +271,7 @@ export Consume # Raymarching algorithms export RaymarchAlgorithm, IsoValue, Absorption, MaximumIntensityProjection, AbsorptionRGBA, IndexedAbsorptionRGBA export Billboard +export NoShading, FastShading, MultiLightShading # Reexports of # Color/Vector types convenient for 3d/2d graphics @@ -346,7 +349,7 @@ export Arrows , Heatmap , Image , Lines , LineSegments , Mesh , MeshScatte export arrows , heatmap , image , lines , linesegments , mesh , meshscatter , poly , scatter , surface , text , volume , wireframe export arrows! , heatmap! , image! , lines! , linesegments! , mesh! , meshscatter! , poly! , scatter! , surface! , text! , volume! , wireframe! -export PointLight, EnvironmentLight, AmbientLight, SSAO +export AmbientLight, PointLight, DirectionalLight, SpotLight, EnvironmentLight, SSAO include("precompiles.jl") diff --git a/src/basic_recipes/band.jl b/src/basic_recipes/band.jl index b0434868c1a..9a8de39b48d 100644 --- a/src/basic_recipes/band.jl +++ b/src/basic_recipes/band.jl @@ -13,7 +13,7 @@ $(ATTRIBUTES) default_theme(scene, Mesh)..., colorrange = automatic, ) - attr[:shading][] = false + attr[:shading][] = NoShading attr end diff --git a/src/basic_recipes/contourf.jl b/src/basic_recipes/contourf.jl index 4936f25d372..0ab2ffe518b 100644 --- a/src/basic_recipes/contourf.jl +++ b/src/basic_recipes/contourf.jl @@ -141,7 +141,7 @@ function Makie.plot!(c::Contourf{<:Tuple{<:AbstractVector{<:Real}, <:AbstractVec color = colors, strokewidth = 0, strokecolor = :transparent, - shading = false, + shading = NoShading, inspectable = c.inspectable, transparency = c.transparency ) diff --git a/src/basic_recipes/contours.jl b/src/basic_recipes/contours.jl index e2701997a87..ccef86c54e8 100644 --- a/src/basic_recipes/contours.jl +++ b/src/basic_recipes/contours.jl @@ -247,10 +247,12 @@ function plot!(plot::T) where T <: Union{Contour, Contour3d} align = (:center, :center), fontsize = labelsize, font = labelfont, + transform_marker = false ) - lift(scene.camera.projectionview, scene.px_area, labels, labelcolor, labelformatter, - lev_pos_col) do _, _, labels, labelcolor, labelformatter, lev_pos_col + lift(scene.camera.projectionview, transformationmatrix(plot), scene.px_area, + labels, labelcolor, labelformatter, lev_pos_col + ) do _, _, _, labels, labelcolor, labelformatter, lev_pos_col labels || return pos = texts.positions.val; empty!(pos) rot = texts.rotation.val; empty!(rot) diff --git a/src/basic_recipes/tooltip.jl b/src/basic_recipes/tooltip.jl index ae0a516f04c..74c75241062 100644 --- a/src/basic_recipes/tooltip.jl +++ b/src/basic_recipes/tooltip.jl @@ -36,13 +36,13 @@ Creates a tooltip pointing at `position` displaying the given `string` """ @recipe(Tooltip, position) do scene Attributes(; - # General - text = "", + # General + text = "", offset = 10, placement = :above, align = 0.5, - xautolimits = false, - yautolimits = false, + xautolimits = false, + yautolimits = false, zautolimits = false, overdraw = false, depth_shift = 0f0, @@ -62,7 +62,7 @@ Creates a tooltip pointing at `position` displaying the given `string` # Background backgroundcolor = :white, triangle_size = 10, - + # Outline outline_color = :black, outline_linewidth = 2f0, @@ -103,7 +103,7 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) text_offset = map(p.offset, textpadding, p.triangle_size, p.placement, p.align) do o, pad, ts, placement, align l, r, b, t = pad - if placement === :left + if placement === :left return Vec2f(-o - r - ts, b - align * (b + t)) elseif placement === :right return Vec2f( o + l + ts, b - align * (b + t)) @@ -118,7 +118,7 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) end text_align = map(p.placement, p.align) do placement, align - if placement === :left + if placement === :left return (1.0, align) elseif placement === :right return (0.0, align) @@ -155,9 +155,9 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) # Text background mesh mesh!( - p, bbox, shading = false, space = :pixel, + p, bbox, shading = NoShading, space = :pixel, color = p.backgroundcolor, fxaa = false, - transparency = p.transparency, visible = p.visible, + transparency = p.transparency, visible = p.visible, overdraw = p.overdraw, depth_shift = p.depth_shift, inspectable = p.inspectable ) @@ -170,8 +170,8 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) ) mp = mesh!( - p, triangle, shading = false, space = :pixel, - color = p.backgroundcolor, + p, triangle, shading = NoShading, space = :pixel, + color = p.backgroundcolor, transparency = p.transparency, visible = p.visible, overdraw = p.overdraw, depth_shift = p.depth_shift, inspectable = p.inspectable @@ -179,8 +179,8 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) onany(bbox, p.triangle_size, p.placement, p.align) do bb, s, placement, align o = origin(bb); w = widths(bb) scale!(mp, s, s, s) - - if placement === :left + + if placement === :left translate!(mp, Vec3f(o[1] + w[1], o[2] + align * w[2], 0)) rotate!(mp, qrotation(Vec3f(0,0,1), 0.5pi)) elseif placement === :right @@ -212,45 +212,45 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) # | ____ # | | - shift = if placement === :left + shift = if placement === :left Vec2f[ - (l, b + 0.5h), (l, t), (r, t), - (r, b + align * h + 0.5s), - (r + s, b + align * h), + (l, b + 0.5h), (l, t), (r, t), + (r, b + align * h + 0.5s), + (r + s, b + align * h), (r, b + align * h - 0.5s), (r, b), (l, b), (l, b + 0.5h) ] elseif placement === :right Vec2f[ - (l + 0.5w, b), (l, b), - (l, b + align * h - 0.5s), - (l-s, b + align * h), + (l + 0.5w, b), (l, b), + (l, b + align * h - 0.5s), + (l-s, b + align * h), (l, b + align * h + 0.5s), (l, t), (r, t), (r, b), (l + 0.5w, b) ] elseif placement in (:below, :down, :bottom) Vec2f[ - (l, b + 0.5h), (l, t), - (l + align * w - 0.5s, t), - (l + align * w, t+s), - (l + align * w + 0.5s, t), + (l, b + 0.5h), (l, t), + (l + align * w - 0.5s, t), + (l + align * w, t+s), + (l + align * w + 0.5s, t), (r, t), (r, b), (l, b), (l, b + 0.5h) ] elseif placement in (:above, :up, :top) Vec2f[ - (l, b + 0.5h), (l, t), (r, t), (r, b), - (l + align * w + 0.5s, b), - (l + align * w, b-s), - (l + align * w - 0.5s, b), + (l, b + 0.5h), (l, t), (r, t), (r, b), + (l + align * w + 0.5s, b), + (l + align * w, b-s), + (l + align * w - 0.5s, b), (l, b), (l, b + 0.5h) ] else @error "Tooltip placement $placement invalid. Assuming :above" Vec2f[ - (l, b + 0.5h), (l, t), (r, t), (r, b), - (l + align * w + 0.5s, b), - (l + align * w, b-s), - (l + align * w - 0.5s, b), + (l, b + 0.5h), (l, t), (r, t), (r, b), + (l + align * w + 0.5s, b), + (l + align * w, b-s), + (l + align * w - 0.5s, b), (l, b), (l, b + 0.5h) ] end @@ -259,8 +259,8 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) end lines!( - p, outline, - color = p.outline_color, space = :pixel, + p, outline, + color = p.outline_color, space = :pixel, linewidth = p.outline_linewidth, linestyle = p.outline_linestyle, transparency = p.transparency, visible = p.visible, overdraw = p.overdraw, depth_shift = p.depth_shift, diff --git a/src/basic_recipes/tricontourf.jl b/src/basic_recipes/tricontourf.jl index e808c34103e..72f90865605 100644 --- a/src/basic_recipes/tricontourf.jl +++ b/src/basic_recipes/tricontourf.jl @@ -4,7 +4,7 @@ struct DelaunayTriangulation end tricontourf(triangles::Triangulation, zs; kwargs...) tricontourf(xs, ys, zs; kwargs...) -Plots a filled tricontour of the height information in `zs` at the horizontal positions `xs` and +Plots a filled tricontour of the height information in `zs` at the horizontal positions `xs` and vertical positions `ys`. A `Triangulation` from DelaunayTriangulation.jl can also be provided instead of `xs` and `ys` for specifying the triangles, otherwise an unconstrained triangulation of `xs` and `ys` is computed. @@ -54,16 +54,16 @@ function Makie.used_attributes(::Type{<:Tricontourf}, ::AbstractVector{<:Real}, return (:triangulation,) end -function Makie.convert_arguments(::Type{<:Tricontourf}, x::AbstractVector{<:Real}, y::AbstractVector{<:Real}, z::AbstractVector{<:Real}; +function Makie.convert_arguments(::Type{<:Tricontourf}, x::AbstractVector{<:Real}, y::AbstractVector{<:Real}, z::AbstractVector{<:Real}; triangulation=DelaunayTriangulation()) z = elconvert(Float32, z) points = [x'; y'] if triangulation isa DelaunayTriangulation tri = DelTri.triangulate(points) elseif !(triangulation isa DelTri.Triangulation) - # Wrap user's provided triangulation into a Triangulation. Their triangulation must be such that DelTri.add_triangle! is defined. - if typeof(triangulation) <: AbstractMatrix{<:Int} && size(triangulation, 1) != 3 - triangulation = triangulation' + # Wrap user's provided triangulation into a Triangulation. Their triangulation must be such that DelTri.add_triangle! is defined. + if typeof(triangulation) <: AbstractMatrix{<:Int} && size(triangulation, 1) != 3 + triangulation = triangulation' end tri = DelTri.Triangulation(points) triangles = DelTri.get_triangles(tri) @@ -198,7 +198,7 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:DelTri.Triangulation, <:AbstractVe color = colors, strokewidth = 0, strokecolor = :transparent, - shading = false, + shading = NoShading, inspectable = c.inspectable, transparency = c.transparency ) diff --git a/src/camera/camera.jl b/src/camera/camera.jl index 56b122d16bd..c86c4e2ed17 100644 --- a/src/camera/camera.jl +++ b/src/camera/camera.jl @@ -1,5 +1,5 @@ function Base.copy(x::Camera) - Camera(ntuple(8) do i + Camera(ntuple(9) do i getfield(x, i) end...) end @@ -18,6 +18,7 @@ function Base.show(io::IO, camera::Camera) println(io, " projection: ", camera.projection[]) println(io, " projectionview: ", camera.projectionview[]) println(io, " resolution: ", camera.resolution[]) + println(io, " lookat: ", camera.lookat[]) println(io, " eyeposition: ", camera.eyeposition[]) end @@ -83,6 +84,7 @@ function Camera(px_area) proj, proj_view, lift(a-> Vec2f(widths(a)), px_area), + Observable(Vec3f(0)), Observable(Vec3f(1)), ObserverFunction[], Dict{Symbol, Observable}() diff --git a/src/camera/camera2d.jl b/src/camera/camera2d.jl index c5d0110744a..93c3783d73f 100644 --- a/src/camera/camera2d.jl +++ b/src/camera/camera2d.jl @@ -11,8 +11,8 @@ end """ cam2d!(scene::SceneLike, kwargs...) -Creates a 2D camera for the given `scene`. The camera implements zooming by -scrolling and translation using mouse drag. It also implements rectangle +Creates a 2D camera for the given `scene`. The camera implements zooming by +scrolling and translation using mouse drag. It also implements rectangle selections. ## Keyword Arguments @@ -46,6 +46,7 @@ function cam2d!(scene::SceneLike; kw_args...) cam end +get_space(::Camera2D) = :data wscale(screenrect, viewrect) = widths(viewrect) ./ widths(screenrect) @@ -54,7 +55,14 @@ wscale(screenrect, viewrect) = widths(viewrect) ./ widths(screenrect) Updates the camera for the given `scene` to cover the given `area` in 2d. """ -update_cam!(scene::SceneLike, area) = update_cam!(scene, cameracontrols(scene), area) +function update_cam!(scene::SceneLike, area::Rect) + return update_cam!(scene, cameracontrols(scene), area) +end +function update_cam!(scene::SceneLike, area::Rect, center::Bool) + return update_cam!(scene, cameracontrols(scene), area, center) +end + + """ update_cam!(scene::SceneLike) @@ -310,6 +318,7 @@ function add_restriction!(cam, window, rarea::Rect2, minwidths::Vec) end struct PixelCamera <: AbstractCamera end +get_space(::PixelCamera) = :pixel struct UpdatePixelCam @@ -317,6 +326,7 @@ struct UpdatePixelCam near::Float32 far::Float32 end +get_space(::UpdatePixelCam) = :pixel function (cam::UpdatePixelCam)(window_size) w, h = Float32.(widths(window_size)) @@ -327,7 +337,7 @@ end """ campixel!(scene; nearclip=-1000f0, farclip=1000f0) -Creates a pixel camera for the given `scene`. This means that the positional +Creates a pixel camera for the given `scene`. This means that the positional data of a plot will be interpreted in pixel units. This camera does not feature controls. """ @@ -345,11 +355,12 @@ function campixel!(scene::Scene; nearclip=-10_000f0, farclip=10_000f0) end struct RelativeCamera <: AbstractCamera end +get_space(::RelativeCamera) = :relative """ cam_relative!(scene) -Creates a camera for the given `scene` which maps the scene area to a 0..1 by +Creates a camera for the given `scene` which maps the scene area to a 0..1 by 0..1 range. This camera does not feature controls. """ function cam_relative!(scene::Scene; nearclip=-10_000f0, farclip=10_000f0) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 11935e7e3bb..5433cd3d9c8 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -1,5 +1,7 @@ abstract type AbstractCamera3D <: AbstractCamera end +get_space(::AbstractCamera3D) = :data + struct Camera3D <: AbstractCamera3D # User settings settings::Attributes @@ -39,6 +41,7 @@ Settings include anything that isn't a mouse or keyboard button. - `zoom_shift_lookat = true`: If true keeps the data under the cursor when zooming. - `cad = false`: If true rotates the view around `lookat` when zooming off-center. - `clipping_mode = :bbox_relative`: Controls how `near` and `far` get processed. With `:static` they get passed as is, with `:view_relative` they get scaled by `norm(eyeposition - lookat)` and with `:bbox_relative` they get scaled to be just outside the scene bounding box. (More specifically `far = 1` is scaled to the furthest point of a bounding sphere and `near` is generally overwritten to be the closest point.) +- `center = true`: Controls whether the camera placement gets reset when calling `update_cam!(scene[, cam], bbox)`, which is called when a new plot is added. This is automatically set to `false` after calling `update_cam!(scene[, cam], eyepos, lookat[, up])`. - `keyboard_rotationspeed = 1f0` sets the speed of keyboard based rotations. - `keyboard_translationspeed = 0.5f0` sets the speed of keyboard based translations. @@ -160,6 +163,7 @@ function Camera3D(scene::Scene; kwargs...) zoom_shift_lookat = true, fixed_axis = true, cad = false, + center = true, clipping_mode = :bbox_relative ) @@ -732,11 +736,12 @@ function update_cam!(scene::Scene, cam::Camera3D) set_proj_view!(camera(scene), proj, view) scene.camera.eyeposition[] = cam.eyeposition[] + scene.camera.lookat[] = cam.lookat[] end # Update camera position via bbox -function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect) +function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect, recenter::Bool = cam.settings.center[]) bb = Rect3f(area3d) width = widths(bb) center = maximum(bb) - 0.5f0 * width @@ -751,9 +756,12 @@ function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect) dist = radius end - cam.lookat[] = center - cam.eyeposition[] = cam.lookat[] .+ dist * old_dir - cam.upvector[] = Vec3f(0, 0, 1) # Should we reset this? + if recenter + cam.lookat[] = center + cam.eyeposition[] = cam.lookat[] .+ dist * old_dir + cam.upvector[] = normalize(cross(old_dir, cross(cam.upvector[], old_dir))) + # Vec3f(0, 0, 1) # Should we reset this? + end if cam.settings.clipping_mode[] === :static cam.near[] = 0.1f0 * dist @@ -767,6 +775,7 @@ end # Update camera position via camera Position & Orientation function update_cam!(scene::Scene, camera::Camera3D, eyeposition::VecTypes, lookat::VecTypes, up::VecTypes = camera.upvector[]) + camera.settings.center[] = false camera.lookat[] = Vec3f(lookat) camera.eyeposition[] = Vec3f(eyeposition) camera.upvector[] = Vec3f(up) @@ -787,6 +796,7 @@ function update_cam!( radius::Real = norm(camera.eyeposition[] - camera.lookat[]), center = camera.lookat[] ) + camera.settings.center[] = false st, ct = sincos(theta) sp, cp = sincos(phi) v = Vec3f(ct * cp, ct * sp, st) @@ -807,4 +817,4 @@ function show_cam(scene) println("cam.upvector[] = ", round.(cam.upvector[], digits=2)) println("cam.fov[] = ", round.(cam.fov[], digits=2)) return -end \ No newline at end of file +end diff --git a/src/camera/old_camera3d.jl b/src/camera/old_camera3d.jl index e3167262327..f7f98e7524a 100644 --- a/src/camera/old_camera3d.jl +++ b/src/camera/old_camera3d.jl @@ -53,6 +53,8 @@ function old_cam3d_cad!(scene::Scene; kw_args...) cam end +get_space(::OldCamera3D) = :data + """ old_cam3d_turntable!(scene; kw_args...) @@ -356,7 +358,9 @@ end Updates the camera's controls to point to the specified location. """ -update_cam!(scene::Scene, eyeposition, lookat, up = Vec3f(0, 0, 1)) = update_cam!(scene, cameracontrols(scene), eyeposition, lookat, up) +function update_cam!(scene::Scene, eyeposition::VecTypes{3}, lookat::VecTypes{3}, up::VecTypes{3} = Vec3f(0, 0, 1)) + return update_cam!(scene, cameracontrols(scene), eyeposition, lookat, up) +end function update_cam!(scene::Scene, camera::OldCamera3D, eyeposition, lookat, up = Vec3f(0, 0, 1)) camera.lookat[] = Vec3f(lookat) diff --git a/src/camera/projection_math.jl b/src/camera/projection_math.jl index 717224c6a6a..5fca35e5957 100644 --- a/src/camera/projection_math.jl +++ b/src/camera/projection_math.jl @@ -344,6 +344,22 @@ function clip_to_space(cam::Camera, space::Symbol) end end +function get_space(scene::Scene) + space = get_space(cameracontrols(scene))::Symbol + space === :data ? (:data,) : (:data, space) +end +get_space(::AbstractCamera) = :data +# TODO: Should this be less specialized? ScenePlot? AbstractPlot? +get_space(plot::Combined) = to_value(get(plot, :space, :data))::Symbol + +is_space_compatible(a, b) = is_space_compatible(get_space(a), get_space(b)) +is_space_compatible(a::Symbol, b::Symbol) = a === b +is_space_compatible(a::Symbol, b::Union{Tuple, Vector}) = a in b +function is_space_compatible(a::Union{Tuple, Vector}, b::Union{Tuple, Vector}) + any(x -> is_space_compatible(x, b), a) +end +is_space_compatible(a::Union{Tuple, Vector}, b::Symbol) = is_space_compatible(b, a) + function project(cam::Camera, input_space::Symbol, output_space::Symbol, pos) input_space === output_space && return to_ndim(Point3f, pos, 0) clip_from_input = space_to_clip(cam, input_space) diff --git a/src/interfaces.jl b/src/interfaces.jl index 34bfcdccf32..32bdfc67ffc 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -221,10 +221,10 @@ function plot!(::Combined{F}) where {F} end end -function connect_plot!(scene::SceneLike, plot::Combined{F}) where {F} - plot.parent = scene +function connect_plot!(parent::SceneLike, plot::Combined{F}) where {F} + plot.parent = parent - apply_theme!(parent_scene(scene), plot) + apply_theme!(parent_scene(parent), plot) t_user = to_value(get(attributes(plot), :transformation, automatic)) if t_user isa Transformation plot.transformation = t_user @@ -236,12 +236,15 @@ function connect_plot!(scene::SceneLike, plot::Combined{F}) where {F} transform!(t, t_user) plot.transformation = t end - obsfunc = connect!(transformation(scene), transformation(plot)) - append!(plot.deregister_callbacks, obsfunc) + if is_space_compatible(plot, parent) + obsfunc = connect!(transformation(parent), transformation(plot)) + append!(plot.deregister_callbacks, obsfunc) + end end plot.model = transformationmatrix(plot) convert_arguments!(plot) calculated_attributes!(Combined{F}, plot) + default_shading!(plot, parent_scene(parent)) plot!(plot) return plot end diff --git a/src/lighting.jl b/src/lighting.jl new file mode 100644 index 00000000000..bcf63018209 --- /dev/null +++ b/src/lighting.jl @@ -0,0 +1,194 @@ +abstract type AbstractLight end + +# GLMakie interface + +# These need to match up with light shaders to differentiate light types +module LightType + const UNDEFINED = 0 + const Ambient = 1 + const PointLight = 2 + const DirectionalLight = 3 + const SpotLight = 4 +end + +# Each light should implement +light_type(::AbstractLight) = LightType.UNDEFINED +light_color(::AbstractLight) = RGBf(0, 0, 0) +# Other attributes need to be handled explicitly in backends + + +""" + AmbientLight(color) <: AbstractLight + +A simple ambient light that uniformly lights every object based on its light color. + +Availability: +- All backends with `shading = FastShading` or `MultiLightShading` +""" +struct AmbientLight <: AbstractLight + color::Observable{RGBf} +end + +light_type(::AmbientLight) = LightType.Ambient +light_color(l::AmbientLight) = l.color[] + + +""" + PointLight(color, position[, attenuation = Vec2f(0)]) + PointLight(color, position, range::Real) + +A point-like light source placed at the given `position` with the given light +`color`. + +Optionally an attenuation parameter can be used to reduce the brightness of the +light source with distance. The reduction is given by +`1 / (1 + attenuation[1] * distance + attenuation[2] * distance^2)`. +Alternatively you can pass a light `range` to generate matching default +attenuation parameters. Note that you may need to set the light intensity, i.e. +the light color to values greater than 1 to get satisfying results. + +Availability: +- GLMakie with `shading = MultiLightShading` +- RPRMakie +""" +struct PointLight <: AbstractLight + color::Observable{RGBf} + position::Observable{Vec3f} + attenuation::Observable{Vec2f} +end + +# no attenuation +function PointLight(color::Union{Colorant, Observable{<: Colorant}}, position::Union{VecTypes{3}, Observable{<: VecTypes{3}}}) + return PointLight(color, position, Vec2f(0)) +end +# automatic attenuation +function PointLight(color::Union{Colorant, Observable{<: Colorant}}, position::Union{VecTypes{3}, Observable{<: VecTypes{3}}}, range::Real) + return PointLight(color, position, default_attenuation(range)) +end + +@deprecate PointLight(position::Union{VecTypes{3}, Observable{<: VecTypes{3}}}, color::Union{Colorant, Observable{<: Colorant}}) PointLight(color, position) + +light_type(::PointLight) = LightType.PointLight +light_color(l::PointLight) = l.color[] + +# fit of values used on learnopengl/ogre3d +function default_attenuation(range::Real) + return Vec2f( + 4.690507869767646 * range ^ -1.009712247799057, + 82.4447791934059 * range ^ -2.0192061630628966 + ) +end + + +""" + DirectionalLight(color, direction[, camera_relative = false]) + +A light type which simulates a distant light source with parallel light rays +going in the given `direction`. + +Availability: +- All backends with `shading = FastShading` or `MultiLightShading` +""" +struct DirectionalLight <: AbstractLight + color::Observable{RGBf} + direction::Observable{Vec3f} + + # Usually a light source is placed in world space, i.e. unrelated to the + # camera. As a default however, we want to make sure that an object is + # always reasonably lit, which requires the light source to move with the + # camera. To keep this in sync in WGLMakie, the calculation needs to happen + # in javascript. This flag notives WGLMakie and other backends that this + # calculation needs to happen. + camera_relative::Bool + + DirectionalLight(col, dir, rel = false) = new(col, dir, rel) +end +light_type(::DirectionalLight) = LightType.DirectionalLight +light_color(l::DirectionalLight) = l.color[] + + +""" + SpotLight(color, position, direction, angles) + +Creates a spot light which illuminates objects in a light cone starting at +`position` pointing in `direction`. The opening angle is defined by an inner +and outer angle given in `angles`, between which the light intensity drops off. + +Availability: +- GLMakie with `shading = MultiLightShading` +- RPRMakie +""" +struct SpotLight <: AbstractLight + color::Observable{RGBf} + position::Observable{Vec3f} + direction::Observable{Vec3f} + angles::Observable{Vec2f} +end + +light_type(::SpotLight) = LightType.SpotLight +light_color(l::SpotLight) = l.color[] + + +""" + EnvironmentLight(intensity, image) + +An environment light that uses a spherical environment map to provide lighting. +See: https://en.wikipedia.org/wiki/Reflection_mapping + +Availability: +- RPRMakie +""" +struct EnvironmentLight <: AbstractLight + intensity::Observable{Float32} + image::Observable{Matrix{RGBf}} +end + + +function get_one_light(lights, Typ) + indices = findall(x-> x isa Typ, lights) + isempty(indices) && return nothing + return lights[indices[1]] +end + +function default_shading!(plot, lights::Vector{<: AbstractLight}) + # if the plot does not have :shading we assume the plot doesn't support it + haskey(plot.attributes, :shading) || return + + # Bad type + shading = to_value(plot.attributes[:shading]) + if !(shading isa MakieCore.ShadingAlgorithm || shading === automatic) + @warn "`shading = $shading` is not valid. Use `automatic`, `NoShading`, `FastShading` or `MultiLightShading`. Defaulting to `automatic`." + shading = automatic + end + + # automatic conversion + if shading === automatic + ambient_count = 0 + dir_light_count = 0 + + for light in lights + if light isa AmbientLight + ambient_count += 1 + elseif light isa DirectionalLight + dir_light_count += 1 + elseif light isa EnvironmentLight + continue + else + plot.attributes[:shading] = MultiLightShading + return + end + if ambient_count > 1 || dir_light_count > 1 + plot.attributes[:shading] = MultiLightShading + return + end + end + + if dir_light_count + ambient_count == 0 + plot.attributes[:shading] = NoShading + else + plot.attributes[:shading] = FastShading + end + end + + return +end \ No newline at end of file diff --git a/src/makielayout/blocks/axis.jl b/src/makielayout/blocks/axis.jl index 598db0a178d..4e4a0e0ad00 100644 --- a/src/makielayout/blocks/axis.jl +++ b/src/makielayout/blocks/axis.jl @@ -193,7 +193,7 @@ function initialize_block!(ax::Axis; palette = nothing) # TODO: replace with mesh, however, CairoMakie needs a poly path for this signature # so it doesn't rasterize the scene - background = poly!(blockscene, scenearea; color=ax.backgroundcolor, inspectable=false, shading=false, strokecolor=:transparent) + background = poly!(blockscene, scenearea; color=ax.backgroundcolor, inspectable=false, shading=NoShading, strokecolor=:transparent) translate!(background, 0, 0, -100) elements[:background] = background diff --git a/src/makielayout/blocks/axis3d.jl b/src/makielayout/blocks/axis3d.jl index 484300f60f0..e8470fcbac4 100644 --- a/src/makielayout/blocks/axis3d.jl +++ b/src/makielayout/blocks/axis3d.jl @@ -41,9 +41,10 @@ function initialize_block!(ax::Axis3) matrices = lift(calculate_matrices, scene, finallimits, scene.px_area, ax.elevation, ax.azimuth, ax.perspectiveness, ax.aspect, ax.viewmode, ax.xreversed, ax.yreversed, ax.zreversed) - on(scene, matrices) do (view, proj, eyepos) + on(scene, matrices) do (model, view, proj, eyepos) cam = camera(scene) Makie.set_proj_view!(cam, proj, view) + scene.transformation.model[] = model cam.eyeposition[] = eyepos end @@ -194,7 +195,7 @@ function calculate_matrices(limits, px_area, elev, azim, perspectiveness, aspect end |> Makie.scalematrix t2 = Makie.translationmatrix(-0.5 .* ws .* scales) - scale_matrix = t2 * s * t + model = t2 * s * t ang_max = 90 ang_min = 0.5 @@ -215,23 +216,16 @@ function calculate_matrices(limits, px_area, elev, azim, perspectiveness, aspect eyepos = Vec3{Float64}(x, y, z) - lookat_matrix = Makie.lookat( - eyepos, - Vec3{Float64}(0, 0, 0), - Vec3{Float64}(0, 0, 1)) + lookat_matrix = lookat(eyepos, Vec3{Float64}(0), Vec3{Float64}(0, 0, 1)) w = width(px_area) h = height(px_area) - view_matrix = lookat_matrix * scale_matrix + projection_matrix = projectionmatrix( + lookat_matrix * model, limits, eyepos, radius, azim, elev, angle, + w, h, scales, viewmode) - projection_matrix = projectionmatrix(view_matrix, limits, eyepos, radius, azim, elev, angle, w, h, scales, viewmode) - - # for eyeposition dependent algorithms, we need to present the position as if - # there was no scaling applied - eyeposition = Vec3f(inv(scale_matrix) * Vec4f(eyepos..., 1)) - - view_matrix, projection_matrix, eyeposition + return model, lookat_matrix, projection_matrix, eyepos end function projectionmatrix(viewmatrix, limits, eyepos, radius, azim, elev, angle, width, height, scales, viewmode) @@ -683,7 +677,7 @@ function add_panel!(scene, ax, dim1, dim2, dim3, limits, min3) faces = [1 2 3; 3 4 1] - panel = mesh!(scene, vertices, faces, shading = false, inspectable = false, + panel = mesh!(scene, vertices, faces, shading = NoShading, inspectable = false, xautolimits = false, yautolimits = false, zautolimits = false, color = attr(:panelcolor), visible = attr(:panelvisible)) return panel diff --git a/src/makielayout/blocks/polaraxis.jl b/src/makielayout/blocks/polaraxis.jl index be7bcef0d9f..8efec507ee0 100644 --- a/src/makielayout/blocks/polaraxis.jl +++ b/src/makielayout/blocks/polaraxis.jl @@ -763,7 +763,7 @@ function draw_axis!(po::PolarAxis, radius_at_origin) visible = po.clip, fxaa = false, transformation = Transformation(), # no polar transform for this - shading = false + shading = NoShading ) # inner clip is a (filled) circle sector which also needs to regenerate with @@ -785,7 +785,7 @@ function draw_axis!(po::PolarAxis, radius_at_origin) visible = po.clip, fxaa = false, transformation = Transformation(), - shading = false + shading = NoShading ) # handle placement with transform diff --git a/src/makielayout/blocks/scene.jl b/src/makielayout/blocks/scene.jl index c4fa81663e6..e5cac177b0f 100644 --- a/src/makielayout/blocks/scene.jl +++ b/src/makielayout/blocks/scene.jl @@ -1,14 +1,9 @@ -function Makie.plot!(lscene::LScene, plot::AbstractPlot) - Makie.plot!(lscene.scene, plot) +function reset_limits!(lscene::LScene) notify(lscene.scene.theme.limits) center!(lscene.scene) - return plot -end - -function Makie.plot!(P::Makie.PlotFunc, ax::LScene, args...; kw_attributes...) - attributes = Makie.Attributes(kw_attributes) - return Makie.plot!(ax, P, attributes, args...) + return end +tightlimits!(::LScene) = nothing # TODO implement!? function initialize_block!(ls::LScene; scenekw = NamedTuple()) blockscene = ls.blockscene @@ -62,8 +57,3 @@ Makie.cam3d!(ax::LScene; kwargs...) = Makie.cam3d!(ax.scene; kwargs...) Makie.cam3d_cad!(ax::LScene; kwargs...) = Makie.cam3d_cad!(ax.scene; kwargs...) Makie.old_cam3d!(ax::LScene; kwargs...) = Makie.old_cam3d!(ax.scene; kwargs...) Makie.old_cam3d_cad!(ax::LScene; kwargs...) = Makie.old_cam3d_cad!(ax.scene; kwargs...) - - -function reset_limits!(ax::LScene) - # TODO -end diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index c8ed4abe2c9..3263f6237c9 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -661,7 +661,7 @@ function RectangleZoom(f::Function, ax::Axis; kw...) faces = [1 2 5; 5 2 6; 2 3 6; 6 3 7; 3 4 7; 7 4 8; 4 1 8; 8 1 5] # plot to blockscene, so ax.scene stays exclusive for user plots # That's also why we need to pass `ax.scene` to _selection_vertices, so it can project to that space - mesh = mesh!(ax.blockscene, selection_vertices, faces, color = (:black, 0.2), shading = false, + mesh = mesh!(ax.blockscene, selection_vertices, faces, color = (:black, 0.2), shading = NoShading, inspectable = false, visible=r.active, transparency=true) # translate forward so selection mesh and frame are never behind data translate!(mesh, 0, 0, 100) diff --git a/src/scenes.jl b/src/scenes.jl index fd8872006d8..1c8504be1ad 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -37,33 +37,6 @@ function SSAO(; radius=nothing, bias=nothing, blur=nothing) return SSAO(_radius, _bias, _blur) end -abstract type AbstractLight end - -""" -A positional point light, shining at a certain color. -Color values can be bigger than 1 for brighter lights. -""" -struct PointLight <: AbstractLight - position::Observable{Vec3f} - radiance::Observable{RGBf} -end - -""" -An environment Light, that uses a spherical environment map to provide lighting. -See: https://en.wikipedia.org/wiki/Reflection_mapping -""" -struct EnvironmentLight <: AbstractLight - intensity::Observable{Float32} - image::Observable{Matrix{RGBf}} -end - -""" -A simple, one color ambient light. -""" -struct AmbientLight <: AbstractLight - color::Observable{RGBf} -end - """ Scene TODO document this @@ -131,7 +104,7 @@ mutable struct Scene <: AbstractScene backgroundcolor::Observable{RGBAf}, visible::Observable{Bool}, ssao::SSAO, - lights::Vector{AbstractLight} + lights::Vector ) scene = new( parent, @@ -148,7 +121,7 @@ mutable struct Scene <: AbstractScene backgroundcolor, visible, ssao, - lights, + convert(Vector{AbstractLight}, lights), Observables.ObserverFunction[], Cycler() ) @@ -272,38 +245,33 @@ function Scene(; end if lights isa Automatic - lightposition = to_value(get(m_theme, :lightposition, nothing)) - if !isnothing(lightposition) - position = if lightposition === :eyeposition - scene.camera.eyeposition - elseif lightposition isa Vec3 - m_theme.lightposition - else - error("Wrong lightposition type, use `:eyeposition` or `Vec3f(...)`") + haskey(m_theme, :lightposition) && @warn("`lightposition` is deprecated. Set `light_direction` instead.") + + if haskey(m_theme, :lights) + copyto!(scene.lights, m_theme.lights[]) + else + haskey(m_theme, :light_direction) || error("Theme must contain `light_direction::Vec3f` or an explicit `lights::Vector`!") + haskey(m_theme, :light_color) || error("Theme must contain `light_color::RGBf` or an explicit `lights::Vector`!") + haskey(m_theme, :camera_relative_light) || @warn("Theme should contain `camera_relative_light::Bool`.") + + if haskey(m_theme, :ambient) + push!(scene.lights, AmbientLight(m_theme[:ambient][])) end - push!(scene.lights, PointLight(position, RGBf(1, 1, 1))) - end - ambient = to_value(get(m_theme, :ambient, nothing)) - if !isnothing(ambient) - push!(scene.lights, AmbientLight(ambient)) + + push!(scene.lights, DirectionalLight( + m_theme[:light_color][], m_theme[:light_direction], + to_value(get(m_theme, :camera_relative_light, false)) + )) end end return scene end -function get_one_light(scene::Scene, Typ) - indices = findall(x-> x isa Typ, scene.lights) - isempty(indices) && return nothing - if length(indices) > 1 - @warn("Only one light supported by backend right now. Using only first light") - end - return scene.lights[indices[1]] -end - -get_point_light(scene::Scene) = get_one_light(scene, PointLight) -get_ambient_light(scene::Scene) = get_one_light(scene, AmbientLight) - +get_directional_light(scene::Scene) = get_one_light(scene.lights, DirectionalLight) +get_point_light(scene::Scene) = get_one_light(scene.lights, PointLight) +get_ambient_light(scene::Scene) = get_one_light(scene.lights, AmbientLight) +default_shading!(plot, scene::Scene) = default_shading!(plot, scene.lights) function Scene( parent::Scene; diff --git a/src/theming.jl b/src/theming.jl index 12893abc60d..dfc8d0656a4 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -66,10 +66,21 @@ const MAKIE_DEFAULT_THEME = Attributes( blur = Int32(2), # A (2blur+1) by (2blur+1) range is used for blurring # N_samples = 64, # number of samples (requires shader reload) ), - ambient = RGBf(0.55, 0.55, 0.55), - lightposition = :eyeposition, inspectable = true, + # Vec is equvalent to 36° right/east, 39° up/north from camera position + # The order here is Vec3f(right of, up from, towards) viewer/camera + light_direction = Vec3f(-0.45679495, -0.6293204, -0.6287243), + camera_relative_light = true, # Only applies to default DirectionalLight + light_color = RGBf(0.5, 0.5, 0.5), + ambient = RGBf(0.35, 0.35, 0.35), + + # Note: this can be set too + # lights = AbstractLight[ + # AmbientLight(RGBf(0.55, 0.55, 0.55)), + # DirectionalLight(RGBf(0.8, 0.8, 0.8), Vec3f(2/3, 2/3, 1/3)) + # ], + CairoMakie = Attributes( px_per_unit = 2.0, pt_per_unit = 0.75, @@ -98,14 +109,17 @@ const MAKIE_DEFAULT_THEME = Attributes( monitor = nothing, visible = true, - # Postproccessor + # Shader constants & Postproccessor oit = true, fxaa = true, ssao = false, # This adjusts a factor in the rendering shaders for order independent # transparency. This should be the same for all of them (within one rendering # pipeline) otherwise depth "order" will be broken. - transparency_weight_scale = 1000f0 + transparency_weight_scale = 1000f0, + # maximum number of lights with shading = :verbose + max_lights = 64, + max_light_parameters = 5 * 64 ), WGLMakie = Attributes( diff --git a/src/types.jl b/src/types.jl index 8e1096d4953..a3144183a7b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -241,7 +241,12 @@ struct Camera resolution::Observable{Vec2f} """ - Eye position of the camera, sued for e.g. ray tracing. + Focal point of the camera, used for e.g. camera synchronized light direction. + """ + lookat::Observable{Vec3f} + + """ + Eye position of the camera, used for e.g. ray tracing. """ eyeposition::Observable{Vec3f} diff --git a/test/scenes.jl b/test/scenes.jl index b28a6dd51b8..d7bb82c6547 100644 --- a/test/scenes.jl +++ b/test/scenes.jl @@ -7,3 +7,59 @@ @test theme(nothing, :nonexistant, default=1) == 1 @test theme(scene, :nonexistant, default=1) == 1 end + +@testset "Lighting" begin + @testset "Shading default" begin + plot = (attributes = Attributes(), ) # simplified "plot" + + # Based on number of lights + lights = Makie.AbstractLight[] + Makie.default_shading!(plot, lights) + @test !haskey(plot.attributes, :shading) + + plot.attributes[:shading] = Observable(Makie.automatic) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === NoShading + + plot.attributes[:shading] = Observable(Makie.automatic) + push!(lights, AmbientLight(RGBf(0.1, 0.1, 0.1))) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === FastShading + + plot.attributes[:shading] = Observable(Makie.automatic) + push!(lights, DirectionalLight(RGBf(0.1, 0.1, 0.1), Vec3f(1))) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === FastShading + + plot.attributes[:shading] = Observable(Makie.automatic) + push!(lights, PointLight(RGBf(0.1, 0.1, 0.1), Point3f(0))) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + # Based on light types + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [SpotLight(RGBf(0.1, 0.1, 0.1), Point3f(0), Vec3f(1), Vec2f(0.2, 0.3))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [EnvironmentLight(1.0, rand(2,2))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === NoShading # only affects RPRMakie so skipped here + + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [PointLight(RGBf(0.1, 0.1, 0.1), Point3f(0))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [PointLight(RGBf(0.1, 0.1, 0.1), Point3f(0), Vec2f(0.1, 0.2))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + # keep existing shading type + lights = Makie.AbstractLight[] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + end +end \ No newline at end of file