#include "pch.h" #include "log.h" #include "canvas.h" #include "app.h" #include "texture.h" #include "node_progress_bar.h" #include "renderer_gl/opengl_capabilities.h" #include #include #include #include #ifdef __APPLE__ #include #import "objc_utils.h" #elif __WEB__ void webgl_sync(); #endif namespace { GLint current_canvas_stroke_internal_format() { if (ShaderManager::ext_float32_linear) return static_cast(pp::renderer::gl::rgba32f_internal_format()); if (ShaderManager::ext_float16) return static_cast(pp::renderer::gl::rgba16f_internal_format()); return static_cast(pp::renderer::gl::rgba8_internal_format()); } GLint rgba8_internal_format() { return static_cast(pp::renderer::gl::rgba8_internal_format()); } GLenum texture_2d_target() { return static_cast(pp::renderer::gl::texture_2d_target()); } GLenum rgba_pixel_format() { return static_cast(pp::renderer::gl::rgba_pixel_format()); } GLenum unsigned_byte_component_type() { return static_cast(pp::renderer::gl::unsigned_byte_component_type()); } GLenum viewport_query() { return static_cast(pp::renderer::gl::viewport_query()); } GLenum color_clear_value_query() { return static_cast(pp::renderer::gl::color_clear_value_query()); } GLenum depth_test_state() { return static_cast(pp::renderer::gl::depth_test_state()); } GLenum scissor_test_state() { return static_cast(pp::renderer::gl::scissor_test_state()); } GLenum blend_state() { return static_cast(pp::renderer::gl::blend_state()); } GLint texture_filter_linear() { return static_cast(pp::renderer::gl::linear_texture_filter()); } GLint texture_filter_linear_mipmap_linear() { return static_cast(pp::renderer::gl::linear_mipmap_linear_texture_filter()); } GLint texture_filter_nearest() { return static_cast(pp::renderer::gl::nearest_texture_filter()); } GLint texture_wrap_repeat() { return static_cast(pp::renderer::gl::repeat_texture_wrap()); } GLint texture_wrap_clamp_to_border() { return static_cast(pp::renderer::gl::clamp_to_border_texture_wrap()); } pp::renderer::gl::OpenGlPixelFormat texture_format_for_image_channels(int channel_count) { return pp::renderer::gl::texture_format_for_channel_count(static_cast(channel_count)); } void set_active_texture_unit(std::uint32_t unit_index) { glActiveTexture(pp::renderer::gl::active_texture_unit(unit_index)); } void unbind_texture_2d() { glBindTexture(texture_2d_target(), 0); } } Canvas* Canvas::I; std::vector Canvas::modes[] = { { new CanvasModePen, new CanvasModeBasicCamera }, // brush { new CanvasModePen, new CanvasModeBasicCamera }, // eraser { new CanvasModeLine, new CanvasModeBasicCamera }, // line { new CanvasModeCamera, new CanvasModeBasicCamera }, // parallax { new CanvasModeGrid, new CanvasModeBasicCamera }, // grids { new CanvasModeTransform, new CanvasModeBasicCamera }, // import { new CanvasModeTransform, new CanvasModeBasicCamera }, // cut { new CanvasModeTransform, new CanvasModeBasicCamera }, // copy { new CanvasModeFill, new CanvasModeBasicCamera }, // fill { new CanvasModeMaskFree, new CanvasModeBasicCamera }, // mask-free { new CanvasModeMaskLine, new CanvasModeBasicCamera }, // mask-poly { new CanvasModeFloodFill, new CanvasModeBasicCamera }, // flood-fill }; glm::vec3 Canvas::m_plane_origin[6] = { { 0, 0,-1}, // front { 1, 0, 0}, // right { 0, 0, 1}, // back {-1, 0, 0}, // left { 0, 1, 0}, // top { 0,-1, 0}, // bottom }; glm::vec3 Canvas::m_plane_normal[6] = { { 0, 0, 1}, // front {-1, 0, 0}, // right { 0, 0,-1}, // back { 1, 0, 0}, // left { 0,-1, 0}, // top { 0, 1, 0}, // bottom }; glm::vec3 Canvas::m_plane_tangent[6] = { {0, 1, 0}, // front {0, 1, 0}, // right {0, 1, 0}, // back {0, 1, 0}, // left {0, 0,-1}, // top {0, 0, 1}, // bottom }; // only rotation glm::mat4 Canvas::m_plane_transform[6] = { glm::lookAt(glm::vec3(), { 0, 0,-1}, {0, 1, 0}), // front glm::lookAt(glm::vec3(), {-1, 0, 0}, {0, 1, 0}), // right glm::lookAt(glm::vec3(), { 0, 0, 1}, {0, 1, 0}), // back glm::lookAt(glm::vec3(), { 1, 0, 0}, {0, 1, 0}), // left glm::lookAt(glm::vec3(), { 0, 1, 0}, {0, 0,-1}), // top glm::lookAt(glm::vec3(), { 0,-1, 0}, {0, 0, 1}), // bottom }; void Canvas::pick_start() { for (int i = 0; i < 6; i++) m_pick_ready[i] = false; } void Canvas::pick_update(int plane) { // check if already updated if (m_pick_ready[plane]) return; App::I->render_task([this, plane] { std::array faces{ false }; faces[plane] = true; draw_merge(true, faces); int i = plane; m_layers_merge.rtt(i).bindFramebuffer(); if (!m_pick_data[plane]) m_pick_data[plane] = std::make_unique(m_width * m_height); glReadPixels( 0, 0, m_width, m_height, rgba_pixel_format(), unsigned_byte_component_type(), m_pick_data[plane].get()); m_layers_merge.rtt(i).unbindFramebuffer(); }); m_pick_ready[plane] = true; } glm::vec4 Canvas::pick_get(glm::vec2 canvas_loc) { glm::vec3 ray_origin; glm::vec3 ray_dir; glm::vec3 hit_pos; glm::vec3 hit_normal; glm::vec2 fb_pos; int plane_id; if (point_trace(canvas_loc, ray_origin, ray_dir, hit_pos, fb_pos, hit_normal, plane_id)) { pick_update(plane_id); int i = (int)fb_pos.x + (int)fb_pos.y * m_width; return glm::vec4(m_pick_data[plane_id][i]) / 255.f; } return {0,0,0,1}; } void Canvas::pick_end() { for (int i = 0; i < 6; i++) m_pick_data[i].release(); } void Canvas::clear(const glm::vec4& c/*={0,0,0,1}*/) { auto a = new ActionLayerClear; a->m_layer = m_layers[m_current_layer_idx]; a->m_frame = layer().m_frame_index; a->m_snap = std::make_shared(a->m_layer->snapshot()); a->m_color = c; ActionManager::add(a); m_layers[m_current_layer_idx]->clear(c); m_unsaved = true; } void Canvas::clear_all() { for (auto& l : m_layers) l->clear({0, 0, 0, 0}); } void Canvas::stroke_end() { if (!m_current_stroke) return; if (m_current_stroke->has_sample()) { m_commit_delayed = true; } else { m_show_tmp = false; stroke_commit(); m_current_stroke = nullptr; } } void Canvas::stroke_cancel() { if (!m_current_stroke) return; m_current_stroke = nullptr; m_show_tmp = false; } void Canvas::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz) { gl_state gl; gl.save(); m_mixer.bindFramebuffer(); glViewport(0, 0, m_mixer.getWidth(), m_mixer.getHeight()); glDisable(depth_test_state()); glEnable(scissor_test_state()); glDisable(blend_state()); glScissor(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y); auto layer_index = m_current_layer_idx; for (int plane_index = 0; plane_index < 6; plane_index++) { if (!m_layers[layer_index]->m_visible || m_layers[layer_index]->m_opacity == .0f || !m_layers[layer_index]->face(plane_index)) continue; //glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)m_mixer.getWidth() / m_mixer.getHeight(), 0.1f, 1000.f); auto plane_mvp_z = glm::scale(glm::vec3(1, -1, 1)) * m_proj * m_mv * m_plane_transform[plane_index] * glm::translate(glm::vec3(0, 0, -1)); m_sampler.bind(0); m_sampler.bind(1); m_sampler.bind(2); const auto& b = m_current_stroke->m_brush; ShaderManager::use(kShader::CompDraw); ShaderManager::u_int(kShaderUniform::Tex, 0); //ShaderManager::u_int(kShaderUniform::TexA, 0); ShaderManager::u_int(kShaderUniform::TexStroke, 1); ShaderManager::u_int(kShaderUniform::TexMask, 2); ShaderManager::u_vec2(kShaderUniform::Resolution, m_size); ShaderManager::u_int(kShaderUniform::TexPattern, 3); ShaderManager::u_float(kShaderUniform::Alpha, 1); ShaderManager::u_int(kShaderUniform::Lock, false/*m_layers[layer_index]->m_alpha_locked*/); ShaderManager::u_int(kShaderUniform::Mask, false/*m_smask_active*/); ShaderManager::u_int(kShaderUniform::UseFragcoord, false); ShaderManager::u_int(kShaderUniform::UseDual, false); ShaderManager::u_int(kShaderUniform::UsePattern, false); ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode); ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z); set_active_texture_unit(0); m_layers[layer_index]->rtt(plane_index).bindTexture(); set_active_texture_unit(1); m_tmp[plane_index].bindTexture(); set_active_texture_unit(2); m_smask.rtt(plane_index).bindTexture(); m_node->m_face_plane.draw_fill(); m_smask.rtt(plane_index).unbindTexture(); set_active_texture_unit(1); m_tmp[plane_index].unbindTexture(); set_active_texture_unit(0); m_layers[layer_index]->rtt(plane_index).unbindTexture(); } m_sampler.unbind(); m_mixer.unbindFramebuffer(); gl.restore(); } std::array, 6> Canvas::stroke_draw_project(std::array& B, bool project_3d /*= false*/, glm::mat4 mv /*= glm::mat4(1)*/) const { // intersect P with the current face to clip diverging points from the plane const auto unp_vp = zw(m_box); const auto unp_inv = glm::inverse(m_proj * m_mv); std::array, 6> ret; for (int i = 0; i < 6; i++) { struct ray_t { glm::vec3 o; glm::vec3 d; vertex_t v; ray_t(glm::vec3 o, glm::vec3 d, vertex_t v) : o(o), d(d), v(v) { } }; std::vector rays; if (project_3d) { rays.reserve(B.size()); for (auto const& b : B) rays.emplace_back(glm::vec3(0), b.pos, b); } else { auto P = poly_intersect(B.data(), B.data() + 4, m_plane_shape[i]); rays.reserve(P.size()); for (auto const& p : P) { glm::vec3 ray_origin, ray_dir; auto clip_space = glm::vec2(p.pos.x, unp_vp.y - p.pos.y - 1.f) / unp_vp * 2.f - 1.f; auto wp0 = unp_inv * glm::vec4(clip_space, 0, 1); auto wp1 = unp_inv * glm::vec4(clip_space, .5, 1); ray_origin = xyz(wp0 / wp0.w); ray_dir = glm::normalize(xyz(wp1 / wp1.w) - ray_origin); rays.emplace_back(ray_origin, ray_dir, p); } } glm::mat4 plane_camera = glm::lookAt(m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i]); std::vector face_ret; face_ret.reserve(rays.size()); for (auto const& r : rays) { glm::vec3 hit; float hit_t; if (ray_intersect(r.o, r.d, m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i], hit, hit_t)) { glm::vec4 plane_local = plane_camera * glm::vec4(hit, 1); //P[j].uvs2 = xy(P[j].pos) / glm::vec2(App::I->width, App::I->height); vertex_t v; v.pos.x = -(plane_local.x * 0.5f - 0.5f) * m_width; v.pos.y = (plane_local.y * 0.5f + 0.5f) * m_height; // Black magic - BEWARE! // interpolation perspective correction, use the current camera projection to correct the interpolation // because the new shape will have z fixed with an ortho projection when drawn to the face // we need to imitate the same perspective as the once in the camera // see: https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/perspective-correct-interpolation-vertex-attributes auto hit_cam = mv * glm::vec4(hit, 1); v.pos.z = 0; v.pos.w = hit_cam.z; v.uvs = r.v.uvs * hit_cam.z; v.uvs2 = r.v.uvs2 * hit_cam.z; face_ret.emplace_back(v); } else { break; } } if (face_ret.size() >= 3) ret[i] = std::move(face_ret); } return ret; } glm::vec4 Canvas::stroke_draw_samples(int i, std::vector& P) { if (!ShaderManager::ext_framebuffer_fetch) { set_active_texture_unit(1); m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing) } glm::vec2 bb_min(m_width, m_height); glm::vec2 bb_max(0, 0); for (int j = 0; j < P.size(); j++) { bb_min = glm::max({ 0, 0 }, glm::min(bb_min, xy(P[j].pos))); bb_max = glm::min({ m_width, m_height }, glm::max(bb_max, xy(P[j].pos))); } auto bb_sz = bb_max - bb_min; glm::vec2 pad(1); glm::ivec2 tex_pos = glm::clamp(glm::floor(bb_min) - pad, { 0, 0 }, { m_width, m_height }); glm::ivec2 tex_sz = glm::clamp(glm::ceil(bb_sz) + pad * 2.f, { 0, 0 }, (glm::vec2)(glm::ivec2(m_width, m_height) - tex_pos)); if (!ShaderManager::ext_framebuffer_fetch) { glCopyTexSubImage2D(texture_2d_target(), 0, tex_pos.x, tex_pos.y, tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y); } if (P.size() == 4) { static vertex_t rect[6]; rect[0] = P[0]; rect[1] = P[1]; rect[2] = P[2]; rect[3] = P[0]; rect[4] = P[2]; rect[5] = P[3]; m_brush_shape.update_vertices(rect, 6); } else if (P.size() == 3) { m_brush_shape.update_vertices(P.data(), (int)P.size()); } else { P = triangulate_simple(P); m_brush_shape.update_vertices(P.data(), (int)P.size()); } m_brush_shape.draw_fill(); if (!ShaderManager::ext_framebuffer_fetch) { set_active_texture_unit(1); m_tex[i].unbind(); } return glm::vec4(tex_pos, tex_pos + tex_sz); } std::vector Canvas::stroke_draw_compute(Stroke& stroke) const { std::vector ret; StrokeSample prev = stroke.m_prev_sample; auto samples = stroke.compute_samples(); std::array B = { vertex_t{ {0, 0, 1, 1}, {0, 0}, {0, 0} }, vertex_t{ {0, 0, 1, 1}, {0, 1}, {0, 1} }, vertex_t{ {0, 0, 1, 1}, {1, 1}, {1, 1} }, vertex_t{ {0, 0, 1, 1}, {1, 0}, {1, 0} }, }; for (const auto& s : samples) { if (!s.valid()) continue; ret.emplace_back(); auto& f = ret.back(); glm::vec2 dx_mix(prev.size * 0.5f, 0), dy_mix(0, prev.size * 0.5f); glm::vec2 off_mix[4] = { -dx_mix - dy_mix, // A - bottom-left -dx_mix + dy_mix, // B - top-left +dx_mix + dy_mix, // C - top-right +dx_mix - dy_mix, // D - bottom-right }; // P is the initial square centered at the cursor location glm::vec2 dx(s.size * 0.5f, 0), dy(0, s.size * 0.5f); glm::vec2 off[4] = { -dx - dy, // A - bottom-left -dx + dy, // B - top-left +dx + dy, // C - top-right +dx - dy, // D - bottom-right }; glm::vec2 mixer_sz(App::I->width, App::I->height); glm::vec2 mixer_bb_min(mixer_sz); glm::vec2 mixer_bb_max(0, 0); for (int j = 0; j < 4; j++) { auto p = (xy(prev.pos) + App::I->zoom * s.scale * off_mix[j] * glm::orientate2(-s.angle)); mixer_bb_min = glm::max({ 0, 0 }, glm::min(mixer_bb_min, p)); mixer_bb_max = glm::min(mixer_sz, glm::max(mixer_bb_max, p)); if (s.pos.z == 0.f) { B[j].pos = glm::vec4(xy(s.pos) + App::I->zoom * s.scale * off[j] * glm::orientate2(-s.angle) - glm::vec2(0, 1), 1, 1); } else { auto m = glm::inverse(glm::lookAt({ 0, 0, 0 }, s.pos, { 0, 1, 0 })); glm::vec3 off_3d = m * glm::vec4(off[j], 0, 1); B[j].pos = glm::vec4(s.pos + off_3d, 1.f); } B[j].uvs2 = p / mixer_sz; } f.m_mixer_rect = glm::vec4(glm::floor(mixer_bb_min), glm::ceil(mixer_bb_max - mixer_bb_min)) / App::I->zoom; f.col = glm::vec4(s.col, 1); f.flow = s.flow; f.opacity = s.opacity; if (s.pos.z == 0.f) { f.shapes = stroke_draw_project(B, false, m_mv); } else { auto m = glm::lookAt({ 0, 0, 0 }, s.pos, { 0, 1, 0 }); f.shapes = stroke_draw_project(B, true, m); } prev = s; } return ret; } void Canvas::stroke_draw() { if (!(m_current_stroke && m_current_stroke->has_sample())) { stroke_commit_timelapse(); //stroke_draw_mix({ 0,0 }, { m_mixer.getWidth(), m_mixer.getHeight() }); return; } m_dirty = true; std::array merge_faces; GLint vp[4]; GLfloat cc[4]; glGetIntegerv(viewport_query(), vp); glGetFloatv(color_clear_value_query(), cc); const auto& brush = m_current_stroke->m_brush; const auto& dual_brush = m_dual_stroke->m_brush; auto ortho_proj = glm::ortho(0.f, (float)m_width, 0.f, (float)m_height, -1.f, 1.f); glViewport(0, 0, m_width, m_height); m_sampler_brush.bind(0); m_sampler_nearest.bind(1); m_sampler_stencil.bind(2); m_sampler.bind(3); //m_sampler_linear.bind(5); glm::vec2 patt_scale = glm::vec2(brush->m_pattern_scale); if (brush->m_pattern_flipx) patt_scale.x *= -1.f; if (brush->m_pattern_flipy) patt_scale.y *= -1.f; glDisable(blend_state()); ShaderManager::use(kShader::Stroke); ShaderManager::u_int(kShaderUniform::Tex, 0); // brush if (!ShaderManager::ext_framebuffer_fetch) ShaderManager::u_int(kShaderUniform::TexBG, 1); // bg ShaderManager::u_int(kShaderUniform::TexPattern, 2); // pattern ShaderManager::u_int(kShaderUniform::TexMix, 3); // mixer //ShaderManager::u_int(kShaderUniform::TexMixA, 4); // mixer ShaderManager::u_float(kShaderUniform::Opacity, brush->m_tip_opacity); ShaderManager::u_vec2(kShaderUniform::Resolution, { m_width, m_height }); ShaderManager::u_vec2(kShaderUniform::PatternScale, patt_scale); ShaderManager::u_float(kShaderUniform::PatternInvert, brush->m_pattern_invert); ShaderManager::u_float(kShaderUniform::PatternBright, brush->m_pattern_brightness); ShaderManager::u_float(kShaderUniform::PatternContrast, brush->m_pattern_contrast); ShaderManager::u_float(kShaderUniform::PatternDepth, brush->m_pattern_depth); ShaderManager::u_int(kShaderUniform::PatternBlendMode, brush->m_pattern_blend_mode); ShaderManager::u_vec2(kShaderUniform::PatternOffset, m_pattern_offset); ShaderManager::u_int(kShaderUniform::UsePattern, brush->m_pattern_enabled && brush->m_pattern_eachsample); ShaderManager::u_float(kShaderUniform::MixAlpha, brush->m_tip_mix); ShaderManager::u_float(kShaderUniform::Wet, brush->m_tip_wet); ShaderManager::u_float(kShaderUniform::Noise, brush->m_tip_noise); ShaderManager::u_mat4(kShaderUniform::MVP, ortho_proj); // DRAW MAIN BRUSH set_active_texture_unit(0); brush->m_tip_texture->bind(); set_active_texture_unit(2); brush->m_pattern_texture ? brush->m_pattern_texture->bind() : unbind_texture_2d(); set_active_texture_unit(3); m_mixer.bindTexture(); auto frames = stroke_draw_compute(*m_current_stroke); std::array box_face = SIXPLETTE(glm::vec4(m_size, 0, 0)); std::array box_dirty = SIXPLETTE(false); glm::vec4 pad_color; for (auto& f : frames) { if (brush->m_tip_mix > 0.f) { stroke_draw_mix(xy(f.m_mixer_rect), zw(f.m_mixer_rect)); } for (int i = 0; i < 6; i++) { auto& P = f.shapes[i]; if (P.size() < 3) continue; m_dirty_face[i] = true; merge_faces[i] = true; box_dirty[i] = true; m_tmp[i].bindFramebuffer(); ShaderManager::use(kShader::Stroke); ShaderManager::u_vec4(kShaderUniform::Col, f.col); ShaderManager::u_float(kShaderUniform::Alpha, f.flow); ShaderManager::u_float(kShaderUniform::Opacity, f.opacity); auto box_sample = stroke_draw_samples(i, P); m_tmp[i].unbindFramebuffer(); m_dirty_box[i] = glm::clamp(box_union(m_dirty_box[i], box_sample), glm::vec4(0), glm::vec4(m_width)); box_face[i] = box_union(box_face[i], box_sample); // TODO: maybe average color? pad_color = f.col; } } set_active_texture_unit(3); m_mixer.unbindTexture(); set_active_texture_unit(0); brush->m_tip_texture->unbind(); // pad stroke // In order to mitigate color bleeding at the edge of shapes in transparent layers // we need to fill the area around the stroke with the same color because by default // the transparent area may have black or other undefined color. // This step is only useful for previewing the stroke because on commit the dilate // algorithm fixes this issue. // NOTE: at the moment this works on the whole canvas, but it can be optimized // to only affect the current dirty box. In this case it may be necessary to do this // work on documents that doesn't have the padding, so on document loading. ShaderManager::use(kShader::StrokePad); ShaderManager::u_vec4(kShaderUniform::Col, pad_color); if (!ShaderManager::ext_framebuffer_fetch) { set_active_texture_unit(1); ShaderManager::u_int(kShaderUniform::TexBG, 1); } for (int i = 0; i < 6; i++) { if (!box_dirty[i]) continue; const auto& b = box_face[i]; glm::vec2 box_size = zw(b) - xy(b); glm::vec2 pad = { 20, 20 }; // pixels padding glm::vec4 pad_box = { glm::max({0, 0}, xy(b) - pad) * 2.f / m_size - 1.f, glm::min(m_size, zw(b) + pad) * 2.f / m_size - 1.f }; // B(xw)--(zw)C box // | // | coordinates // A(xy)--(zy)D mapping std::array pad_quad = { vertex_t({pad_box.x, pad_box.y}), // A vertex_t({pad_box.x, pad_box.w}), // B vertex_t({pad_box.z, pad_box.w}), // C vertex_t({pad_box.x, pad_box.y}), // A vertex_t({pad_box.z, pad_box.w}), // C vertex_t({pad_box.z, pad_box.y}), // D }; m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size()); m_tmp[i].bindFramebuffer(); if (!ShaderManager::ext_framebuffer_fetch) { glm::vec2 o = glm::max({0, 0}, xy(b) - pad); glm::vec2 sz = glm::min(m_size, zw(b) + pad) - o; m_tex[i].bind(); if (sz.x > 0 && sz.y > 0) glCopyTexSubImage2D(texture_2d_target(), 0, o.x, o.y, o.x, o.y, sz.x, sz.y); } m_brush_shape.draw_fill(); m_tmp[i].unbindFramebuffer(); } if (!ShaderManager::ext_framebuffer_fetch) { unbind_texture_2d(); } // DRAW DUAL BRUSH if (brush->m_dual_enabled) { ShaderManager::use(kShader::Stroke); ShaderManager::u_int(kShaderUniform::UsePattern, false); ShaderManager::u_float(kShaderUniform::MixAlpha, 0); ShaderManager::u_float(kShaderUniform::Wet, 0); ShaderManager::u_float(kShaderUniform::Noise, 0); set_active_texture_unit(0); dual_brush->m_tip_texture ? dual_brush->m_tip_texture->bind() : unbind_texture_2d(); auto frames_dual = stroke_draw_compute(*m_dual_stroke); for (auto& f : frames_dual) { ShaderManager::u_vec4(kShaderUniform::Col, f.col); ShaderManager::u_float(kShaderUniform::Alpha, f.flow); ShaderManager::u_float(kShaderUniform::Opacity, f.opacity); for (int i = 0; i < 6; i++) { auto& P = f.shapes[i]; if (P.size() < 3) continue; m_tmp_dual[i].bindFramebuffer(); auto box_sample = stroke_draw_samples(i, P); m_tmp_dual[i].unbindFramebuffer(); // this mode overflows the main brush boundries if (brush->m_dual_blend_mode == 0) m_dirty_box[i] = glm::clamp(box_union(m_dirty_box[i], box_sample), glm::vec4(0), glm::vec4(m_width)); } } } m_sampler_brush.unbind(); m_sampler_nearest.unbind(); m_sampler_stencil.unbind(); glViewport(vp[0], vp[1], vp[2], vp[3]); glClearColor(cc[0], cc[1], cc[2], cc[3]); if (m_commit_delayed) { m_show_tmp = false; m_commit_delayed = false; stroke_commit(); m_current_stroke = nullptr; } } bool Canvas::point_trace(glm::vec2 loc, glm::vec3& ray_origin, glm::vec3& ray_dir, glm::vec3& hit_pos, glm::vec2& fb_pos, glm::vec3& hit_normal, int& out_plane_id) { point_unproject(loc, { 0, 0, zw(m_box) }, m_mv, m_proj, ray_origin, ray_dir); glm::vec3 hit; float hit_t; for (int i = 0; i < 6; i++) { if (ray_intersect(ray_origin, ray_dir, m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i], hit, hit_t)) { glm::mat4 plane_camera = glm::lookAt(m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i]); glm::vec4 plane_local = plane_camera * glm::vec4(hit, 1); if (glm::abs(plane_local.x) < 1.f && glm::abs(plane_local.y) < 1.f) { fb_pos.x = -(plane_local.x * 0.5f - 0.5f) * m_width; fb_pos.y = (plane_local.y * 0.5f + 0.5f) * m_height; hit_pos = hit; hit_normal = m_plane_normal[i]; out_plane_id = i; return true; } else continue; } else continue; } return false; } /* bool Canvas::point_trace_plane(glm::vec2 loc, glm::vec3& hit_pos, glm::vec2& hit_fb_pos, int plane_id) { auto ln = (loc / zw(m_box)) * 2.f - 1.f; auto p = m_plane_unproject[plane_id] * glm::vec4(ln, 1, 1); if (p.w <= 0) { return true; } return false; } */ bool Canvas::point_trace_plane(glm::vec2 loc, glm::vec3& ray_origin, glm::vec3& ray_dir, glm::vec3& hit_pos, glm::vec3& hit_normal, glm::vec2& hit_fb_pos, int plane_id) { point_unproject(loc, { 0, 0, zw(m_box) }, m_mv, m_proj, ray_origin, ray_dir); glm::vec3 hit; float hit_t; if (ray_intersect(ray_origin, ray_dir, m_plane_origin[plane_id], m_plane_normal[plane_id], m_plane_tangent[plane_id], hit, hit_t)) { glm::mat4 plane_camera = glm::lookAt(m_plane_origin[plane_id], m_plane_normal[plane_id], m_plane_tangent[plane_id]); glm::vec4 plane_local = plane_camera * glm::vec4(hit, 1); hit_pos = hit; hit_normal = m_plane_normal[plane_id]; hit_fb_pos.x = -(plane_local.x * 0.5f - 0.5f); hit_fb_pos.y = (plane_local.y * 0.5f + 0.5f); return true; } return false; } void Canvas::point_unproject(glm::vec2 loc, glm::vec4 vp, glm::mat4 camera, glm::mat4 proj, glm::vec3& out_origin, glm::vec3& out_dir) { auto clip_space = glm::vec2(loc.x, vp.w - loc.y - 1.f) / zw(vp) * 2.f - 1.f; auto inv = glm::inverse(proj * camera); auto wp0 = inv * glm::vec4(clip_space, 0, 1); auto wp1 = inv * glm::vec4(clip_space, .5, 1); out_origin = xyz(wp0 / wp0.w); out_dir = glm::normalize(xyz(wp1 / wp1.w) - out_origin); }; void Canvas::point_unproject(glm::vec2 loc, glm::vec3& out_origin, glm::vec3& out_dir) { auto clip_space = glm::vec2(loc.x, m_vp.w - loc.y - 1.f) / zw(m_vp) * 2.f - 1.f; auto inv = glm::inverse(m_proj * m_mv); auto wp0 = inv * glm::vec4(clip_space, 0, 1); auto wp1 = inv * glm::vec4(clip_space, .5, 1); out_origin = xyz(wp0 / wp0.w); out_dir = glm::normalize(xyz(wp1 / wp1.w) - out_origin); } glm::vec3 Canvas::point_trace(glm::vec2 loc) { glm::vec3 ray_origin; glm::vec3 ray_dir; glm::vec3 hit_pos; glm::vec3 hit_normal; glm::vec2 fb_pos; int plane_id; if (point_trace(loc, ray_origin, ray_dir, hit_pos, fb_pos, hit_normal, plane_id)) return hit_pos; return glm::vec3(0); } void Canvas::stroke_commit() { if (!m_dirty || m_layers.empty()) return; m_dirty = false; m_dirty_stroke = true; // new stroke ready for timelapse capture App::I->redraw = true; // save viewport and clear color states GLint vp[4]; GLfloat cc[4]; glGetIntegerv(viewport_query(), vp); glGetFloatv(color_clear_value_query(), cc); auto blend = glIsEnabled(blend_state()); // allocate action to add to history auto action = new ActionStroke; action->was_saved = !m_unsaved; m_unsaved = true; App::I->title_update(); // prepare common states glViewport(0, 0, m_width, m_height); glDisable(blend_state()); const auto& b = m_current_stroke->m_brush; for (int i = 0; i < 6; i++) { //m_dirty_box[i] = glm::vec4(0, 0, m_width, m_height); // reset bounding box if (!m_dirty_face[i]) continue; // no stroke on this face, skip it m_layers[m_current_layer_idx]->rtt(i).bindFramebuffer(); // save image before commit glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]); action->m_image[i] = std::make_unique(box_sz.x * box_sz.y * 4); glReadPixels( m_dirty_box[i].x, m_dirty_box[i].y, box_sz.x, box_sz.y, rgba_pixel_format(), unsigned_byte_component_type(), action->m_image[i].get()); action->m_box[i] = m_dirty_box[i]; action->m_old_box[i] = m_layers[m_current_layer_idx]->box(i); action->m_old_dirty[i] = m_layers[m_current_layer_idx]->face(i); if (!m_layers[m_current_layer_idx]->m_alpha_locked) { auto& lbox = m_layers[m_current_layer_idx]->box(i); lbox = glm::vec4( glm::min(xy(m_dirty_box[i]), xy(lbox)), glm::max(zw(m_dirty_box[i]), zw(lbox)) ); } m_layers[m_current_layer_idx]->face(i) = true; // copy to tmp2 for layer blending set_active_texture_unit(0); m_tex2[i].bind(); glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height); m_tex2[i].unbind(); m_tmp[i].bindTexture(); set_active_texture_unit(1); m_tex2[i].bind(); m_sampler.bind(0); m_sampler_nearest.bind(1); m_sampler.bind(2); m_sampler.bind(3); m_sampler_stencil.bind(4); if (m_current_mode == kCanvasMode::Erase) { ShaderManager::use(kShader::CompErase); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_int(kShaderUniform::TexStroke, 1); ShaderManager::u_int(kShaderUniform::TexMask, 2); ShaderManager::u_int(kShaderUniform::Mask, m_smask_active); ShaderManager::u_float(kShaderUniform::Alpha, 1); ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f)); set_active_texture_unit(0); m_tex2[i].bind(); set_active_texture_unit(1); m_tmp[i].bindTexture(); set_active_texture_unit(2); m_smask.rtt(i).bindTexture(); m_plane.draw_fill(); m_smask.rtt(i).unbindTexture(); set_active_texture_unit(1); m_tmp[i].unbindTexture(); set_active_texture_unit(0); m_tex2[i].unbind(); } else { glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale); if (b->m_pattern_flipx) patt_scale.x *= -1.f; if (b->m_pattern_flipy) patt_scale.y *= -1.f; ShaderManager::use(kShader::CompDraw); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_int(kShaderUniform::TexStroke, 1); ShaderManager::u_int(kShaderUniform::TexMask, 2); ShaderManager::u_int(kShaderUniform::TexDual, 3); ShaderManager::u_int(kShaderUniform::TexPattern, 4); ShaderManager::u_vec2(kShaderUniform::Resolution, m_size); ShaderManager::u_float(kShaderUniform::Alpha, 1); ShaderManager::u_int(kShaderUniform::Mask, m_smask_active); ShaderManager::u_int(kShaderUniform::Lock, m_layers[m_current_layer_idx]->m_alpha_locked); ShaderManager::u_int(kShaderUniform::UseFragcoord, false); ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode); ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f)); ShaderManager::u_int(kShaderUniform::UseDual, b->m_dual_enabled); ShaderManager::u_int(kShaderUniform::DualBlendMode, b->m_dual_blend_mode); ShaderManager::u_float(kShaderUniform::DualAlpha, b->m_dual_opacity); ShaderManager::u_int(kShaderUniform::UsePattern, b->m_pattern_enabled && !b->m_pattern_eachsample); ShaderManager::u_vec2(kShaderUniform::PatternScale, patt_scale); ShaderManager::u_float(kShaderUniform::PatternInvert, b->m_pattern_invert); ShaderManager::u_float(kShaderUniform::PatternBright, b->m_pattern_brightness); ShaderManager::u_float(kShaderUniform::PatternContrast, b->m_pattern_contrast); ShaderManager::u_float(kShaderUniform::PatternDepth, b->m_pattern_depth); ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode); ShaderManager::u_vec2(kShaderUniform::PatternOffset, m_pattern_offset); set_active_texture_unit(0); m_tex2[i].bind(); set_active_texture_unit(1); m_tmp[i].bindTexture(); set_active_texture_unit(2); m_smask.rtt(i).bindTexture(); set_active_texture_unit(3); if (b->m_dual_enabled) m_tmp_dual[i].bindTexture(); set_active_texture_unit(4); b->m_pattern_texture ? b->m_pattern_texture->bind() : unbind_texture_2d(); m_plane.draw_fill(); set_active_texture_unit(3); if (b->m_dual_enabled) m_tmp_dual[i].unbindTexture(); set_active_texture_unit(2); m_smask.rtt(i).unbindTexture(); set_active_texture_unit(1); m_tmp[i].unbindTexture(); set_active_texture_unit(0); m_tex2[i].unbind(); } // else // { // ShaderManager::use(kShader::StrokeLayer); // ShaderManager::u_int(kShaderUniform::TexBG, 1); // ShaderManager::u_int(kShaderUniform::Lock, m_layers[m_current_layer_idx]->m_alpha_locked); // ShaderManager::u_float(kShaderUniform::Alpha, b->m_tip_opacity); // // ShaderManager::u_int(kShaderUniform::Tex, 0); // ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f)); // m_plane.draw_fill(); // m_sampler.unbind(); // m_sampler_bg.unbind(); // m_tex2[i].unbind(); // m_tmp[i].unbindTexture(); // } // Dilate borders to avoid interpolation bleeding ShaderManager::use(kShader::StrokeDilate); ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f)); ShaderManager::u_int(kShaderUniform::TexBG, 0); set_active_texture_unit(0); m_tex2[i].bind(); glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height); m_plane.draw_fill(); m_layers[m_current_layer_idx]->rtt(i).unbindFramebuffer(); } // restore viewport and clear color states blend ? glEnable(blend_state()) : glDisable(blend_state()); glViewport(vp[0], vp[1], vp[2], vp[3]); glClearColor(cc[0], cc[1], cc[2], cc[3]); set_active_texture_unit(0); // save history action->m_layer_idx = m_current_layer_idx; action->m_frame_idx = layer().m_frame_index; action->m_canvas = this; //action->m_stroke = std::move(m_current_stroke); ActionManager::add(action); stroke_commit_timelapse(); } void Canvas::stroke_commit_timelapse() { if (m_encoder && App::I->rec_running) { auto t_now = std::chrono::high_resolution_clock::now(); float dt = std::chrono::duration(t_now - m_disrty_stroke_time).count(); if (dt > 2.f && m_dirty_stroke && App::I->rec_mutex.try_lock()) { draw_merge(true); App::I->rec_mutex.unlock(); App::I->rec_cv.notify_one(); LOG("rec frame generated"); m_dirty_stroke = false; m_disrty_stroke_time = std::chrono::steady_clock::now(); } } } void Canvas::draw_merge(bool draw_checkerboard, std::array faces /*= SIXPLETTE(false)*/) { assert(App::I->is_render_thread()); glViewport(0, 0, m_width, m_height); auto ortho = glm::ortho(-0.5f, 0.5f, -0.5f, 0.5f, -1.f, 1.f); const auto& b = m_current_stroke->m_brush; // check if any layer use blend, otherwise draw directly on main framebuffer bool use_blend = false; for (auto& l : m_layers) { use_blend |= l->m_blend_mode != 0; } if (Canvas::I->m_current_stroke) use_blend |= Canvas::I->m_current_stroke->m_brush->m_blend_mode != 0; // if not using shader blend, use gl rasterizer blend glDisable(depth_test_state()); for (int plane_index = 0; plane_index < 6; plane_index++) { if (!faces[plane_index]) continue; m_layers_merge.rtt(plane_index).bindFramebuffer(); m_layers_merge.rtt(plane_index).clear({ 1, 1, 1, 0 }); if (use_blend) { glDisable(blend_state()); m_layers_merge.rtt(plane_index).clear(); } else { if (draw_checkerboard) { ShaderManager::use(kShader::Checkerboard); ShaderManager::u_int(kShaderUniform::Colorize, false); ShaderManager::u_mat4(kShaderUniform::MVP, ortho); m_plane.draw_fill(); } glEnable(blend_state()); } for (int layer_index = 0; layer_index < m_layers.size(); layer_index++) { if (!(m_show_tmp && m_current_layer_idx == layer_index) && (!m_layers[layer_index]->m_visible || m_layers[layer_index]->m_opacity == .0f || !m_layers[layer_index]->face(plane_index))) continue; if (use_blend) { m_merge_rtt.bindFramebuffer(); m_merge_rtt.clear(); } if (m_current_stroke && m_current_mode == kCanvasMode::Erase && m_show_tmp && m_current_layer_idx == layer_index) { m_sampler.bind(0); m_sampler.bind(1); m_sampler.bind(2); ShaderManager::use(kShader::CompErase); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_int(kShaderUniform::TexStroke, 1); ShaderManager::u_int(kShaderUniform::TexMask, 2); //ShaderManager::u_vec2(kShaderUniform::Resolution, zw(m_box) / zoom); ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity); //ShaderManager::u_int(kShaderUniform::Lock, m_layers[layer_index]->m_alpha_locked); ShaderManager::u_int(kShaderUniform::Mask, m_smask_active); ShaderManager::u_mat4(kShaderUniform::MVP, ortho); set_active_texture_unit(0); m_layers[layer_index]->rtt(plane_index).bindTexture(); set_active_texture_unit(1); m_tmp[plane_index].bindTexture(); set_active_texture_unit(2); m_smask.rtt(plane_index).bindTexture(); m_plane.draw_fill(); m_smask.rtt(plane_index).unbindTexture(); set_active_texture_unit(1); m_tmp[plane_index].unbindTexture(); set_active_texture_unit(0); m_layers[layer_index]->rtt(plane_index).unbindTexture(); } else if (m_current_stroke && m_show_tmp && m_current_layer_idx == layer_index) { m_sampler.bind(0); m_sampler.bind(1); m_sampler.bind(2); m_sampler.bind(3); m_sampler_stencil.bind(4); glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale); if (b->m_pattern_flipx) patt_scale.x *= -1.f; if (b->m_pattern_flipy) patt_scale.y *= -1.f; ShaderManager::use(kShader::CompDraw); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_int(kShaderUniform::TexStroke, 1); ShaderManager::u_int(kShaderUniform::TexMask, 2); ShaderManager::u_int(kShaderUniform::TexDual, 3); ShaderManager::u_vec2(kShaderUniform::Resolution, Canvas::I->m_size); ShaderManager::u_int(kShaderUniform::TexPattern, 4); ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity); ShaderManager::u_int(kShaderUniform::Lock, m_layers[layer_index]->m_alpha_locked); ShaderManager::u_int(kShaderUniform::Mask, m_smask_active); ShaderManager::u_int(kShaderUniform::UseFragcoord, false); ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode); ShaderManager::u_mat4(kShaderUniform::MVP, ortho); ShaderManager::u_int(kShaderUniform::UseDual, b->m_dual_enabled); ShaderManager::u_int(kShaderUniform::DualBlendMode, b->m_dual_blend_mode); ShaderManager::u_int(kShaderUniform::UsePattern, b->m_pattern_enabled && !b->m_pattern_eachsample); ShaderManager::u_vec2(kShaderUniform::PatternScale, patt_scale); ShaderManager::u_float(kShaderUniform::PatternInvert, b->m_pattern_invert); ShaderManager::u_float(kShaderUniform::PatternBright, b->m_pattern_brightness); ShaderManager::u_float(kShaderUniform::PatternContrast, b->m_pattern_contrast); ShaderManager::u_float(kShaderUniform::PatternDepth, b->m_pattern_depth); ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode); ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset); ShaderManager::u_float(kShaderUniform::DualAlpha, b->m_dual_opacity); set_active_texture_unit(0); m_layers[layer_index]->rtt(plane_index).bindTexture(); set_active_texture_unit(1); m_tmp[plane_index].bindTexture(); set_active_texture_unit(2); m_smask.rtt(plane_index).bindTexture(); set_active_texture_unit(3); if (b->m_dual_enabled) m_tmp_dual[plane_index].bindTexture(); set_active_texture_unit(4); b->m_pattern_texture ? b->m_pattern_texture->bind() : unbind_texture_2d(); m_plane.draw_fill(); set_active_texture_unit(3); if (b->m_dual_enabled) m_tmp_dual[plane_index].unbindTexture(); set_active_texture_unit(2); m_smask.rtt(plane_index).unbindTexture(); set_active_texture_unit(1); m_tmp[plane_index].unbindTexture(); set_active_texture_unit(0); m_layers[layer_index]->rtt(plane_index).unbindTexture(); } else { m_cam_fov < 20.f ? m_sampler_nearest.bind(0) : m_sampler.bind(0); ShaderManager::use(kShader::TextureAlpha); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity); ShaderManager::u_int(kShaderUniform::Highlight, m_layers[layer_index]->m_hightlight); ShaderManager::u_mat4(kShaderUniform::MVP, ortho); set_active_texture_unit(0); m_layers[layer_index]->rtt(plane_index).bindTexture(); m_plane.draw_fill(); m_layers[layer_index]->rtt(plane_index).unbindTexture(); } if (use_blend) { m_merge_rtt.unbindFramebuffer(); } // draw the blended if (use_blend) { m_sampler.bind(0); m_sampler.bind(2); ShaderManager::use(kShader::TextureBlend); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode); ShaderManager::u_float(kShaderUniform::Alpha, 1.f); ShaderManager::u_mat4(kShaderUniform::MVP, ortho); if (!ShaderManager::ext_framebuffer_fetch) { m_sampler.bind(2); ShaderManager::u_int(kShaderUniform::TexBG, 2); } set_active_texture_unit(0); m_merge_rtt.bindTexture(); if (!ShaderManager::ext_framebuffer_fetch) { set_active_texture_unit(2); m_merge_tex.bind(); glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height); } m_plane.draw_fill(); if (!ShaderManager::ext_framebuffer_fetch) { set_active_texture_unit(2); m_merge_tex.unbind(); } set_active_texture_unit(0); m_merge_rtt.unbindTexture(); } } set_active_texture_unit(2); m_merge_tex.bind(); glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height); // draw the grid behind the layers using a temporary copy if (use_blend) { glEnable(blend_state()); //draw the grid if (draw_checkerboard) { ShaderManager::use(kShader::Checkerboard); ShaderManager::u_int(kShaderUniform::Colorize, false); ShaderManager::u_mat4(kShaderUniform::MVP, ortho); m_plane.draw_fill(); } // draw the layers m_sampler.bind(0); set_active_texture_unit(0); m_merge_tex.bind(); ShaderManager::use(kShader::Texture); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_mat4(kShaderUniform::MVP, ortho); m_plane.draw_fill(); m_merge_tex.unbind(); } m_layers_merge.rtt(plane_index).unbindFramebuffer(); } } void Canvas::stroke_update(glm::vec3 point, float pressure) { m_current_stroke->add_point(point, pressure); if (m_dual_stroke) m_dual_stroke->add_point(point, pressure); } void Canvas::stroke_start(glm::vec3 point, float pressure) { assert(App::I->is_render_thread()); // need to commit this now before starting a new stroke if (m_current_stroke && m_commit_delayed) { m_show_tmp = false; m_commit_delayed = false; stroke_commit(); m_current_stroke = nullptr; m_dual_stroke = nullptr; } m_pattern_offset = m_current_brush->m_pattern_rand_offset ? glm::vec2((rand()%1000)*0.001f, (rand()%1000)*0.001f) : glm::vec2(0); m_current_stroke = std::make_unique(); m_current_stroke->m_camera.rot = m_cam_rot; m_current_stroke->m_camera.fov = m_cam_fov; if (m_current_mode == kCanvasMode::Line) m_current_stroke->m_filter_points = false; m_current_stroke->randomize_prng(); m_current_stroke->start(m_current_brush); m_current_stroke->add_point(point, pressure); // Generate a brush for the dual-brush if (m_current_brush->m_dual_enabled) { auto dual_brush = std::make_shared(); dual_brush->m_tip_flow = m_current_brush->m_dual_flow; dual_brush->m_tip_opacity = m_current_brush->m_dual_opacity; dual_brush->m_tip_flipx = m_current_brush->m_dual_flipx; dual_brush->m_tip_flipy = m_current_brush->m_dual_flipy; dual_brush->m_tip_invert = m_current_brush->m_dual_invert; dual_brush->m_blend_mode = m_current_brush->m_dual_blend_mode; dual_brush->m_tip_randflipx = m_current_brush->m_dual_randflip; dual_brush->m_tip_randflipy = m_current_brush->m_dual_randflip; dual_brush->m_tip_size = m_current_brush->m_dual_size * m_current_brush->m_tip_size; dual_brush->m_tip_spacing = m_current_brush->m_dual_spacing; dual_brush->m_jitter_scatter = m_current_brush->m_dual_scatter; dual_brush->m_jitter_scatter_bothaxis = m_current_brush->m_dual_scatter_bothaxis; dual_brush->m_jitter_angle = m_current_brush->m_dual_rotate; dual_brush->m_tip_texture = m_current_brush->m_dual_texture; dual_brush->m_tip_aspect = m_current_brush->m_dual_aspect; m_dual_stroke = std::make_unique(); m_dual_stroke->m_camera.rot = m_cam_rot; m_dual_stroke->m_camera.fov = m_cam_fov; m_dual_stroke->start(dual_brush); m_dual_stroke->add_point(point, pressure); } auto const& l = m_layers[m_current_layer_idx]; for (int i = 0; i < 6; i++) { m_dirty_box[i] = glm::vec4(m_width, m_height, 0, 0); // reset bounding box m_dirty_face[i] = false; m_tmp[i].bindFramebuffer(); m_tmp[i].clear({ 0, 0, 0, 0 }); m_tmp[i].unbindFramebuffer(); if (m_current_brush->m_dual_enabled) { m_tmp_dual[i].bindFramebuffer(); m_tmp_dual[i].clear({ 0, 0, 0, 0 }); m_tmp_dual[i].unbindFramebuffer(); } } m_mixer.bindFramebuffer(); m_mixer.clear(); m_mixer.unbindFramebuffer(); m_show_tmp = true; } void Canvas::layer_add(std::string name, std::shared_ptr layer /*= nullptr*/, int index /*= 0*/) { LOG("canvas layer_add %s", name.c_str()); int idx = (int)m_layers.size(); if (layer) { m_layers.insert(m_layers.begin() + index, layer); } else { m_layers.insert(m_layers.begin() + index, std::make_unique()); m_layers[index]->create(m_width, m_height, name); } m_current_layer_idx = index; } std::shared_ptr Canvas::layer_with_id(uint32_t id) { auto layer = std::find_if(m_layers.begin(), m_layers.end(), [id](const auto& l) { return l->id == id; }); if (layer != m_layers.end()) return *layer; return nullptr; } void Canvas::layer_remove(int idx) // m_order index { LOG("canvas layer_remove %d", idx); //m_layers[n]->destroy(); m_layers.erase(m_layers.begin() + idx); m_current_layer_idx = std::min((int)m_layers.size() - 1, idx); } void Canvas::layer_order(int idx, int pos) // m_order index { auto from = m_layers.begin() + idx; auto to = m_layers.begin() + pos; if (pos < idx) std::rotate(to, from, from + 1); if (pos > idx) std::rotate(from, from + 1, to + 1); if (m_current_layer_idx == idx) m_current_layer_idx = pos; else if (m_current_layer_idx == pos) m_current_layer_idx = idx; } void Canvas::layer_merge(int source_idx, int dest_idx) // m_layer index { m_dirty = false; App::I->render_task([&] { // prepare common states glViewport(0, 0, m_width, m_height); glDisable(blend_state()); for (int i = 0; i < 6; i++) { if (!m_layers[source_idx]->face(i)) continue; // no stroke on this face, skip it m_layers[dest_idx]->rtt(i).bindFramebuffer(); auto& lbox = m_layers[dest_idx]->box(i); lbox = glm::vec4( glm::min(xy(m_layers[source_idx]->box(i)), xy(lbox)), glm::max(zw(m_layers[source_idx]->box(i)), zw(lbox)) ); m_layers[dest_idx]->face(i) = true; // copy to tmp2 for layer blending set_active_texture_unit(0); m_tex2[i].bind(); glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height); m_tex2[i].unbind(); m_sampler.bind(0); m_sampler_nearest.bind(1); { ShaderManager::use(kShader::CompDraw); ShaderManager::u_int(kShaderUniform::Tex, 0); // dest ShaderManager::u_int(kShaderUniform::TexStroke, 1); // source ShaderManager::u_vec2(kShaderUniform::Resolution, m_size); ShaderManager::u_float(kShaderUniform::Alpha, m_layers[source_idx]->m_opacity); ShaderManager::u_int(kShaderUniform::Lock, false); ShaderManager::u_int(kShaderUniform::UseFragcoord, false); ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[source_idx]->m_blend_mode); ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f)); ShaderManager::u_int(kShaderUniform::UseDual, false); ShaderManager::u_int(kShaderUniform::UsePattern, false); set_active_texture_unit(0); m_tex2[i].bind(); set_active_texture_unit(1); m_layers[source_idx]->rtt(i).bindTexture(); m_plane.draw_fill(); m_layers[source_idx]->rtt(i).unbindTexture(); set_active_texture_unit(0); m_tex2[i].unbind(); } m_layers[dest_idx]->rtt(i).unbindFramebuffer(); } }); } int Canvas::anim_duration() const noexcept { int frames = 1; for (auto& l : m_layers) frames = glm::max(frames, l->total_duration()); return frames; } void Canvas::anim_update() noexcept { for (auto& l : m_layers) l->goto_frame(m_anim_frame); } void Canvas::anim_goto_frame(int frame) noexcept { m_anim_frame = frame; for (auto& l : m_layers) l->goto_frame(frame); } void Canvas::anim_goto_next() noexcept { anim_goto_frame((m_anim_frame + 1) % anim_duration()); } void Canvas::anim_goto_prev() noexcept { int k = anim_duration(); anim_goto_frame((m_anim_frame - 1 + k) % k); } void Canvas::flood_fill(int layer, int plane, std::vector pos, FloodData& plane_data, float threshold, glm::vec4 dest_color, std::unique_ptr& source_color) { struct adj_t { int plane; bool flipx; bool flipy; bool flipcoord; adj_t(int plane, bool flipx, bool flipy, int flipcoord) : plane(plane), flipx(flipx), flipy(flipy), flipcoord(flipcoord) { } glm::ivec2 compute(glm::ivec2 p, glm::ivec2 sz) const { glm::ivec2 ret; ret[flipcoord] = flipx ? sz.x - p.x : p.x; ret[1 - flipcoord] = flipy ? sz.y - p.y : p.y; return ret; } }; LOG("flood_fill plane %d", plane); auto& rtt = m_layers[layer]->rtt(plane); auto sz = rtt.getSize(); if (!plane_data.mask[plane]) { plane_data.mask[plane] = std::make_unique((size_t)sz.x * sz.y); plane_data.rgb[plane] = std::unique_ptr( reinterpret_cast(m_layers[layer]->rtt(plane).readTextureData())); plane_data.bb[plane] = { sz.x, sz.y, 0, 0 }; plane_data.dirty[plane] = false; plane_data.layer = m_layers[layer]; } auto& mask = plane_data.mask[plane]; auto& rgb = plane_data.rgb[plane]; if (!source_color) source_color = std::make_unique(rgb[pos.back().y * sz.x + pos.back().x]); const glm::vec4 c = *source_color; std::array, 4> edges; static const std::array adj[6] = { // front { adj_t(3, 1, 0, 0), adj_t(4, 1, 1, 0), adj_t(1, 0, 0, 0), adj_t(5, 1, 0, 0), }, // right { adj_t(0, 1, 0, 0), adj_t(4, 1, 0, 1), adj_t(2, 0, 0, 0), adj_t(5, 0, 0, 1), }, // back { adj_t(1, 1, 0, 0), adj_t(4, 0, 0, 0), adj_t(3, 0, 0, 0), adj_t(5, 0, 1, 0), }, // left { adj_t(2, 1, 0, 0), adj_t(4, 0, 1, 1), adj_t(0, 0, 0, 0), adj_t(5, 1, 1, 1), }, // top { adj_t(1, 1, 1, 1), adj_t(0, 1, 1, 0), adj_t(3, 1, 0, 1), adj_t(2, 0, 1, 0), }, // bottom { adj_t(1, 0, 0, 1), adj_t(2, 0, 0, 0), adj_t(3, 0, 1, 1), adj_t(0, 1, 0, 0), }, }; auto test = [&](glm::ivec2 p, bool set_color) -> bool { int i = p.y * sz.x + p.x; if (p.x < 0) { edges[0].push_back(adj[plane][0].compute({ -p.x, p.y }, sz)); return false; } else if (p.x >= sz.x) { edges[2].push_back(adj[plane][2].compute({ sz.x - p.x + 1, p.y }, sz)); return false; } else if (p.y < 0) { edges[3].push_back(adj[plane][3].compute({ p.x, -p.y }, sz)); return false; } else if (p.y >= sz.y) { edges[1].push_back(adj[plane][1].compute({ p.x, sz.y - p.y + 1 }, sz)); return false; } if (!mask[i]) { if (c.a == 0 && glm::abs(rgb[i].a - c.a) < threshold || c.a > 0 && rgb[i].a > 0 && glm::distance(glm::vec3(c), glm::vec3(rgb[i])) < threshold) { if (set_color) { mask[i] = true; rgb[i] = dest_color * 255.f; plane_data.dirty[plane] = true; glm::vec2 bb_min = glm::min((glm::vec2)p, xy(plane_data.bb[plane])); // add 1 pixel to the end because 1 pixel has min(0) and max(1) glm::vec2 bb_max = glm::max((glm::vec2)p + glm::vec2(1), zw(plane_data.bb[plane])); plane_data.bb[plane] = { bb_min, bb_max }; } pos.push_back(p); } return true; } return false; }; while (!pos.empty()) { auto p = pos.back(); pos.pop_back(); if(!test(p + glm::ivec2( 0, 0), true)) continue; test(p + glm::ivec2(-1, 0), false); test(p + glm::ivec2(1, 0), false); test(p + glm::ivec2(0, 1), false); test(p + glm::ivec2(0, -1), false); //for (int x = -1; x <= 1; x++) // for (int y = -1; y <= 1; y++) // if (x != 0 && y != 0) // test(p + glm::ivec2(x, y), false); } for (int i = 0; i < 4; i++) { if (!edges[i].empty()) { flood_fill(layer, adj[plane][i].plane, edges[i], plane_data, threshold, dest_color, source_color); //LOG("continue to plane %d -> %d", plane, adj[plane][i].plane); } } } void Canvas::FloodData::apply() { for (int plane = 0; plane < 6; plane++) { if (!dirty[plane]) continue; auto& rtt = layer->rtt(plane); App::I->render_task([&] { rtt.bindTexture(); glTexSubImage2D(texture_2d_target(), 0, 0, 0, rtt.getWidth(), rtt.getHeight(), rgba_pixel_format(), unsigned_byte_component_type(), rgb[plane].get()); rtt.unbindTexture(); }); layer->face(plane) = true; layer->box(plane) = box_union(layer->box(plane), bb[plane]); } Canvas::I->m_unsaved = true; } void Canvas::resize(int width, int height) { m_width = width; m_height = height; m_size = { width, height }; for (int i = 0; i < 6; i++) { const auto stroke_format = current_canvas_stroke_internal_format(); m_tmp[i].create(width, height, -1, stroke_format); m_tmp_dual[i].create(width, height, -1, stroke_format); m_tex[i].create(width, height, rgba8_internal_format()); m_tex2[i].create(width, height, rgba8_internal_format()); } for (auto& l : m_layers) l->resize(width, height); m_smask.create(width, height, "mask"); m_layers_merge.resize(width, height); m_merge_rtt.create(width, height); m_merge_tex.create(width, height); m_unsaved = true; } void Canvas::destroy() { for (int i = 0; i < 6; i++) { m_tmp[i].destroy(); m_tmp_dual[i].destroy(); m_tex[i].destroy(); m_tex2[i].destroy(); } for (auto& l : m_layers) l->destroy(); m_smask.destroy(); m_mixer.destroy(); m_layers_merge.destroy(); m_merge_rtt.destroy(); m_merge_tex.destroy(); } bool Canvas::create(int width, int height) { m_width = width; m_height = height; m_size = { width, height }; for (int i = 0; i < 6; i++) { const auto stroke_format = current_canvas_stroke_internal_format(); m_tmp[i].create(width, height, -1, stroke_format); m_tmp_dual[i].create(width, height, -1, stroke_format); m_tex[i].create(width, height, rgba8_internal_format()); m_tex2[i].create(width, height, rgba8_internal_format()); } #if defined(__GLES__) m_sampler_brush.create(); #else m_sampler_brush.create(texture_filter_linear(), texture_wrap_clamp_to_border()); #endif m_sampler.create(texture_filter_linear()); m_sampler_nearest.create(texture_filter_nearest()); m_sampler_brush.set_filter(texture_filter_linear_mipmap_linear(), texture_filter_linear()); m_sampler_brush.set_border({ 1, 1, 1, 1 }); m_sampler_stencil.create(texture_filter_linear(), texture_wrap_repeat()); m_sampler_mix.create(texture_filter_nearest(), texture_wrap_repeat()); m_sampler_linear.create(); m_plane.create<1>(1, 1); m_plane_brush.create<1>(1, 1); m_brush_shape.create(); for (auto& l : m_layers) l->create(width, height, ""); m_smask.create(width, height, "mask"); //m_smask.clear({1, 1, 1, 1}); m_layers_merge.create(width, height, "merge"); m_merge_rtt.create(width, height); m_merge_tex.create(width, height); m_unsaved = true; timelapse_reset_encoder(); return true; } void Canvas::snapshot_save() { LOG("SAVE SNAPSHOT"); m_layers_snapshot.clear(); m_layers_snapshot.resize(m_layers.size()); for (int i = 0; i < m_layers.size(); i++) m_layers_snapshot[i] = m_layers[i]->snapshot(); } void Canvas::snapshot_restore() { LOG("RESTORE SNAPSHOT"); for (int i = 0; i < m_layers.size(); i++) m_layers[i]->restore(m_layers_snapshot[i]); m_layers_snapshot.clear(); } void Canvas::clear_context() { LOG("Canvas CLEAR CONTEXT"); for (auto& layer : m_layers) layer->destroy(); for (int i = 0; i < 6; i++) { m_tmp[i].destroy(); m_tex[i].destroy(); m_tex2[i].destroy(); } }; void Canvas::import_equirectangular(std::string file_path, std::shared_ptr layer /*= nullptr*/) { if (App::I->check_license()) { std::thread t([=] { BT_SetTerminate(); import_equirectangular_thread(file_path, layer); }); t.detach(); } } void Canvas::import_equirectangular_thread(std::string file_path, std::shared_ptr layer /*= nullptr*/, int frame /*= -1*/) { Image img; if (!img.load_file(file_path)) return; if (!layer) layer = m_layers[m_current_layer_idx]; if (frame == -1) frame = layer->m_frame_index; auto a = new ActionImportEquirect; a->m_layer = layer; a->m_frame = frame; a->m_snap = std::make_shared(layer->snapshot(frame)); a->m_path = file_path; ActionManager::add(a); m_unsaved = true; if (img.width == img.height / 6) { Texture2D tex; static const GLint indices[] = { 5, 0, 4, 1, 2, 3 }; const auto texture_format = texture_format_for_image_channels(img.comp); tex.create( img.width, img.width, static_cast(texture_format.internal_format), static_cast(texture_format.pixel_format)); int stride = img.width * img.width * img.comp; Plane plane; plane.create<1>(2, 2); draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) { glDisable(GL_DEPTH_TEST); tex.update(img.m_data.get() + indices[i] * stride); m_sampler.bind(0); glActiveTexture(GL_TEXTURE0); tex.bind(); ShaderManager::use(kShader::Texture); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_mat4(kShaderUniform::MVP, glm::scale(glm::vec3(-1, -1, 1))); plane.draw_fill(); tex.unbind(); m_sampler.unbind(); }, frame, false); plane.destroy(); } else { Texture2D tex; tex.load_file(file_path); Sphere sphere; sphere.create<64, 64>(2.f); draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) { glDisable(GL_DEPTH_TEST); m_sampler.bind(0); glActiveTexture(GL_TEXTURE0); tex.bind(); ShaderManager::use(kShader::Texture); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera * glm::eulerAngleY(glm::radians(180.f)) * glm::scale(glm::vec3(1, -1, 1))); sphere.draw_fill(); tex.unbind(); m_sampler.unbind(); }, frame, false); sphere.destroy(); } for (int i = 0; i < 6; i++) { layer->box(i) = glm::vec4(0, 0, m_width, m_height); layer->face(i) = true; } } void Canvas::export_equirectangular(std::string file_path, std::function on_complete) { if (App::I->check_license()) { std::thread t([=] { BT_SetTerminate(); export_equirectangular_thread(file_path); if (on_complete) on_complete(); }); t.detach(); } } void Canvas::export_equirectangular_thread(std::string file_path) { Image data; App::I->render_task([&] { draw_merge(false); Texture2D equirect = m_layers_merge.gen_equirect(); data = equirect.get_image(); }); LOG("writing %s", file_path.c_str()); if (file_path.substr(file_path.size() - 4) == ".jpg") { data.save_jpg(file_path, 100); inject_xmp(file_path); } else if (file_path.substr(file_path.size() - 4) == ".png") { data.save_png(file_path); } #ifdef __IOS__ save_image_library(file_path); #endif } void Canvas::inject_xmp(std::string jpg_path) { static const char xmp[] = "http://ns.adobe.com/xap/1.0/\0" R"( equirectangular True 0 0 0 0 0 PanoPainter )"; FILE* fp = fopen(jpg_path.c_str(), "rb"); fseek(fp, 0, SEEK_END); long len = ftell(fp); fseek(fp, 0, SEEK_SET); unsigned char* jpeg_data = (unsigned char*)malloc(len); fread(jpeg_data, len, 1, fp); fclose(fp); fp = fopen(jpg_path.c_str(), "wb"); int i = 0; while (i < len && !(jpeg_data[i] == 0xff && jpeg_data[i + 1] == 0xd8)) i++; i += 2; unsigned char* xmp_section = (unsigned char*)malloc(sizeof(xmp) + 4); xmp_section[0] = 0xff; xmp_section[1] = 0xe1; xmp_section[2] = ((int)sizeof(xmp) + 2) >> 8; xmp_section[3] = ((int)sizeof(xmp) + 2) >> 0; memcpy(xmp_section + 4, xmp, sizeof(xmp)); fwrite(jpeg_data, 1, i, fp); fwrite(xmp_section, 1, sizeof(xmp) + 4, fp); fwrite(jpeg_data + i, 1, len - i, fp); fclose(fp); } void Canvas::export_depth(std::string file_name, std::function on_complete) { if (App::I->check_license()) { std::thread t([=] { BT_SetTerminate(); export_depth_thread(file_name); if (on_complete) on_complete(); }); t.detach(); } } void Canvas::export_depth_thread(std::string file_name) { RTT rtt; rtt.create(1024, 1024); glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)rtt.getWidth() / (float)rtt.getHeight(), 0.1f, 100.f); glm::mat4 camera = m_cam_rot; App::I->render_task([&] { draw_merge(false); rtt.bindFramebuffer(); rtt.clear({ 0, 0, 0, 1 }); glEnable(GL_BLEND); glDisable(GL_DEPTH_TEST); glViewport(0, 0, rtt.getWidth(), rtt.getHeight()); for (int plane_index = 0; plane_index < 6; plane_index++) { auto plane_mvp_z = proj * camera * m_plane_transform[plane_index] * glm::translate(glm::vec3(0, 0, -1)) * glm::scale(glm::vec3(2)); m_sampler.bind(0); ShaderManager::use(kShader::TextureAlpha); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_float(kShaderUniform::Alpha, 1.f); ShaderManager::u_int(kShaderUniform::Highlight, false); ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z); glActiveTexture(GL_TEXTURE0); m_layers_merge.rtt(plane_index).bindTexture(); m_plane.draw_fill(); m_layers_merge.rtt(plane_index).unbindTexture(); } rtt.unbindFramebuffer(); }); uint8_t* rgba_data = rtt.readTextureData(); stbi_flip_vertically_on_write(true); std::string path_rgba = App::I->work_path + "/" + file_name + ".png"; stbi_write_jpg(path_rgba.c_str(), rtt.getWidth(), rtt.getHeight(), 4, rgba_data, 100); delete rgba_data; App::I->render_task([&] { rtt.bindFramebuffer(); rtt.clear({ 0, 0, 0, 1 }); glEnable(GL_BLEND); glDisable(GL_DEPTH_TEST); glViewport(0, 0, rtt.getWidth(), rtt.getHeight()); for (int layer_index = 0; layer_index < m_layers.size(); layer_index++) { for (int plane_index = 0; plane_index < 6; plane_index++) { if ((!m_layers[layer_index]->m_visible || m_layers[layer_index]->m_opacity == .0f || !m_layers[layer_index]->face(plane_index))) continue; auto plane_mvp_z = proj * camera * m_plane_transform[plane_index] * glm::translate(glm::vec3(0, 0, -1)) * glm::scale(glm::vec3(2)); m_sampler.bind(0); ShaderManager::use(kShader::TextureColorize); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_vec4(kShaderUniform::Col, { glm::vec3((float)(layer_index + 1) / (float)(m_layers.size() + 1)), 1.f }); ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z); glActiveTexture(GL_TEXTURE0); m_layers[layer_index]->rtt(plane_index).bindTexture(); m_plane.draw_fill(); m_layers[layer_index]->rtt(plane_index).unbindTexture(); } } rtt.unbindFramebuffer(); }); uint8_t* depth_data = rtt.readTextureData(); std::string path_depth = App::I->work_path + "/" + file_name + "_depth.png"; stbi_write_jpg(path_depth.c_str(), rtt.getWidth(), rtt.getHeight(), 4, depth_data, 100); delete depth_data; stbi_flip_vertically_on_write(false); rtt.destroy(); } void Canvas::export_layers(std::string path, std::function on_complete) { if (App::I->check_license()) { std::thread t([=] { BT_SetTerminate(); export_layers_thread(path); if (on_complete) on_complete(); }); t.detach(); } } void Canvas::export_layers_thread(std::string path) { auto pb = App::I->show_progress("Export Layers", m_layers.size()); for (int i = 0; i < m_layers.size(); i++) { auto l = m_layers[i]; Image img = l->gen_equirect().get_image(); img.save_png(fmt::format("{}-layer{:02d}-{}.png", path, i, l->m_name)); pb->increment(); } pb->destroy(); } void Canvas::export_anim_frames(std::string path, std::function on_complete) { if (App::I->check_license()) { std::thread t([=] { BT_SetTerminate(); export_anim_frames_thread(path); if (on_complete) on_complete(); }); t.detach(); } } void Canvas::export_anim_frames_thread(std::string path) { auto pb = App::I->show_progress("Export Frames", anim_duration()); for (int i = 0; i < anim_duration(); i++) { anim_goto_frame(i); export_equirectangular_thread(fmt::format("{}-{:02d}.png", path, i)); pb->increment(); } pb->destroy(); } void Canvas::export_anim_mp4(std::string path, std::function on_complete) { if (App::I->check_license()) { std::thread t([=] { BT_SetTerminate(); export_anim_mp4_thread(path); if (on_complete) on_complete(); }); t.detach(); } } void Canvas::export_anim_mp4_thread(std::string path) { auto pb = App::I->show_progress("Export Animation", anim_duration()); int fps = App::I->animation->get_fps(); MP4Encoder mp4; int res = std::min(1024, m_width); mp4.init(res * 4, res * 2, 30, 2 << 20); for (int i = 0; i < anim_duration(); i++) { Image data; App::I->render_task([&] { anim_goto_frame(i); draw_merge(false); Texture2D equirect = m_layers_merge.gen_equirect({ res, res }); data = equirect.get_image(); }); for (int j = 0; j < 30/fps; j++) mp4.encode(data); pb->increment(); } mp4.write_mp4(path); pb->destroy(); } void Canvas::export_cube_faces(std::string file_name, std::function on_complete) { if (App::I->check_license()) { std::thread t([=] { BT_SetTerminate(); export_cube_faces_thread(file_name); if (on_complete) on_complete(); }); t.detach(); } } void Canvas::export_cube_faces_thread(std::string file_name) { #ifdef __OBJC__ NSMutableArray* files = [NSMutableArray array]; #endif static std::array plane_names{ "front", "right", "back", "left", "top", "bottom" }; auto pb = App::I->show_progress("Export Cube Faces", 7); App::I->render_task([this] { draw_merge(false); }); pb->increment(); for (int i = 0; i < 6; i++) { Image face = m_layers_merge.rtt(i).get_image(); std::string path = fmt::format("{}/{}-{}.png", App::I->work_path, file_name, plane_names[i]); face.save_png(path); pb->increment(); #ifdef __IOS__ save_image_library(path); #endif #ifdef __OBJC__ [files addObject : [NSString stringWithUTF8String:path.c_str()] ]; #endif } pb->destroy(); #ifdef __OBJC__ static char name[128]; sprintf(name, "%s.zip", App::I->work_path.c_str()); auto zip_path = [NSString stringWithUTF8String:name]; //[SSZipArchive createZipFileAtPath:zip_path withFilesAtPaths:files]; //for (NSString* f : files) // [[NSFileManager defaultManager] removeItemAtPath:f error:nil]; #endif } void Canvas::project_save(std::function on_complete) { if (App::I->check_license()) { std::thread t([=] { BT_SetTerminate(); bool ret = project_save_thread(App::I->doc_path, true); if (on_complete) on_complete(ret); }); t.detach(); } } void Canvas::project_save(std::string file_path, std::function on_complete) { LOG("saving %s", file_path.c_str()); if (App::I->check_license()) { std::thread t([=] { BT_SetTerminate(); bool ret = project_save_thread(file_path, true); if (on_complete) on_complete(ret); }); t.detach(); } else { LOG("no license, no save"); } } bool Canvas::project_save_thread(std::string file_path, bool show_progress) { // already saved, nothing to do if (!m_unsaved && file_path == App::I->doc_path) { LOG("already saved"); return true; } // static char name[128]; // sprintf(name, "%s/latlong.ppi", data_path.c_str()); FILE* fp; auto start = file_path.rfind('/') + 1; std::string file_name = file_path.substr(start, file_path.length() - start - strlen(".ppi")); std::string tmp_path = App::I->data_path + '/' + file_name + ".tmp.ppi"; std::string lapse_path = App::I->data_path + '/' + file_name + ".pptl"; LOG("file name %s", file_name.c_str()); LOG("tmp path %s", tmp_path.c_str()); bool use_tmp = false; // check if file already exists if ((fp = fopen(file_path.c_str(), "rb"))) { fclose(fp); LOG("use tmp file"); // use tmp file for writing use_tmp = true; if (!(fp = fopen(tmp_path.c_str(), "wb"))) { LOG("cannot write tmp project to %s", tmp_path.c_str()); use_tmp = false; } } LOG("save first time"); if (!fp) { // write directly to the new file if (!(fp = fopen(file_path.c_str(), "wb"))) { LOG("cannot write project to %s", file_path.c_str()); return false; } LOG("unsafe mode saving directly to %s", file_path.c_str()); } PPIHeader ppi_header; fwrite(&ppi_header, sizeof(PPIHeader), 1, fp); // load thumbnail Image thumb = thumbnail_generate(ppi_header.thumb_header.width, ppi_header.thumb_header.height); std::shared_ptr pb; if (show_progress) pb = App::I->show_progress("Saving Pano Project"); thumb.flip(); fwrite(thumb.data(), thumb.size(), 1, fp); fwrite(&m_width, sizeof(int), 1, fp); fwrite(&m_height, sizeof(int), 1, fp); int n_layers = (int)m_layers.size(); fwrite(&n_layers, sizeof(int), 1, fp); int n_frames = std::accumulate(m_layers.begin(), m_layers.end(), 0, [](int tot, auto& l) { return tot + l->frames_count(); }); if (ppi_header.doc_version.minor >= 3) fwrite(&n_frames, sizeof(int), 1, fp); int progress = 0; int total = n_frames * 6; for (int i = 0; i < (int)m_layers.size(); i++) { int n_order = i; fwrite(&n_order, sizeof(int), 1, fp); float layer_alpha = m_layers[i]->m_opacity; fwrite(&layer_alpha, sizeof(float), 1, fp); int name_len = (int)m_layers[i]->m_name.size(); fwrite(&name_len, sizeof(int), 1, fp); fwrite(m_layers[i]->m_name.data(), name_len, 1, fp); if (ppi_header.doc_version.minor >= 2) { fwrite(&m_layers[i]->m_blend_mode, sizeof(int), 1, fp); fwrite(&m_layers[i]->m_alpha_locked, sizeof(bool), 1, fp); fwrite(&m_layers[i]->m_visible, sizeof(bool), 1, fp); } int frames = 1; if (ppi_header.doc_version.minor >= 3) { frames = (int)m_layers[i]->frames_count(); fwrite(&frames, sizeof(int), 1, fp); } for (int fi = 0; fi < frames; fi++) { if (ppi_header.doc_version.minor >= 3) { int duration = m_layers[i]->frame_duration(fi); fwrite(&duration, sizeof(int), 1, fp); } bool gpu = m_layers[i]->frame(fi).gpu_load(); m_layers[i]->optimize(fi); auto snap = m_layers[i]->snapshot(fi); for (int plane_index = 0; plane_index < 6; plane_index++) { int has_data = snap.m_dirty_face[plane_index] ? 1 : 0; fwrite(&has_data, sizeof(int), 1, fp); if (has_data) { glm::ivec4 b = snap.m_dirty_box[plane_index]; glm::vec2 sz = zw(b) - xy(b); int box[4] = { b.x, b.y, b.z, b.w }; fwrite(&box, sizeof(box), 1, fp); std::vector compressed; auto callback = [](void* context, void* data, int size) { std::vector* buffer = static_cast*>(context); buffer->insert(buffer->end(), (uint8_t*)data, (uint8_t*)data + size); }; int ret = stbi_write_png_to_func(callback, &compressed, sz.x, sz.y, 4, snap.image[plane_index].get(), sz.x * 4); int data_size = (int)compressed.size(); fwrite(&data_size, sizeof(int), 1, fp); fwrite(compressed.data(), 1, compressed.size(), fp); } progress++; float p = (float)progress / total * 100.f; if (show_progress) pb->m_progress->SetWidthP(p); LOG("progress: %f", p); } if (!gpu) m_layers[i]->frame(fi).gpu_unload(); } } if (ppi_header.doc_version.minor >= 4) { BinaryStreamWriter sw; sw.init(BinaryStream::ByteOrder::LittleEndian); Serializer::Descriptor info; info.class_id = "ppi_info"; info.name = L"info header"; //info.props["has_encoder"] = std::make_shared(m_encoder != nullptr); sw << info; //if (m_encoder != nullptr) // sw << *m_encoder; int bytes = sw.m_data.size(); fwrite(&bytes, sizeof(int), 1, fp); fwrite((char*)sw.m_data.data(), sw.m_data.size(), 1, fp); } fclose(fp); bool success = false; if (use_tmp) { LOG("project saved tmp to %s", tmp_path.c_str()); LOG("swapping to %s", file_path.c_str()); if (std::remove(file_path.c_str()) == 0) { if (std::rename(tmp_path.c_str(), file_path.c_str()) == 0) { success = true; LOG("tmp file swapped succesfully"); } else { success = false; LOG("tmp file NOT swapped, original removed"); } } else { success = false; LOG("could not remove %s", file_path.c_str()); } } else { success = true; LOG("project saved to %s", file_path.c_str()); } if (success) { m_unsaved = false; m_newdoc = false; // save timelapse if (Canvas::I->m_encoder) { BinaryStreamWriter sw; sw.init(BinaryStream::ByteOrder::LittleEndian); Serializer::Descriptor info; info.class_id = "tracks-info"; info.name = L"Timelapse Tracks"; info.props["has-track-360"] = std::make_shared(true); info.props["version"] = std::make_shared(1); sw << info; sw << *Canvas::I->m_encoder; if (!sw.save(lapse_path)) LOG("cannot save timelase to %s", lapse_path.c_str()); } #if __WEB__ webgl_sync(); #endif } if (show_progress) pb->destroy(); App::I->title_update(); return success; } void Canvas::project_open(std::string file_path, std::function on_complete) { std::thread t([=] { BT_SetTerminate(); bool result = project_open_thread(file_path); if (on_complete) on_complete(result); }); t.detach(); } bool Canvas::project_open_thread(std::string file_path) { FILE* fp = fopen(file_path.c_str(), "rb"); if (!fp) { LOG("cannot write project to %s", file_path.c_str()); return false; // should probably return a bool } PPIHeader ppi_header; fread(&ppi_header, sizeof(PPIHeader), 1, fp); if (!ppi_header.valid()) { LOG("INVALID PPI HEADER"); return false; } std::shared_ptr pb; if (App::I->layout.m_loaded) { pb = std::make_shared(); pb->set_manager(&App::I->layout); pb->init(); pb->create(); pb->loaded(); pb->m_progress->SetWidthP(0); pb->m_title->set_text("Opening Pano Project"); App::I->layout[App::I->main_id]->add_child(pb); } // skip thumbnail Image thumb; thumb.width = ppi_header.thumb_header.width; thumb.height = ppi_header.thumb_header.height; thumb.comp = ppi_header.thumb_header.comp; fseek(fp, thumb.size(), SEEK_CUR); fread(&m_width, sizeof(int), 1, fp); fread(&m_height, sizeof(int), 1, fp); int n_layers = 0; fread(&n_layers, sizeof(int), 1, fp); int n_frames = 1; if (ppi_header.doc_version.minor >= 3) fread(&n_frames, sizeof(int), 1, fp); const int bytes = m_width * m_height * 4; LayerFrame::Snapshot snap; snap.create(m_width, m_height); // allocate single data, no box should be bigger int progress = 0; int total = n_frames * 6; for (auto& l : m_layers) l->destroy(); m_layers.clear(); //clear_all(); resize(m_width, m_height); std::vector> tmp_layers(n_layers); for (int i = 0; i < n_layers; i++) { int n_order; fread(&n_order, sizeof(int), 1, fp); //if (ppi_header.doc_version.minor > 1) // n_order = i; tmp_layers[n_order] = std::make_unique(); auto& layer = tmp_layers[n_order]; fread(&layer->m_opacity, sizeof(float), 1, fp); int name_len; fread(&name_len, sizeof(int), 1, fp); std::string name(name_len, '\0'); fread((char*)name.data(), name_len, 1, fp); if (ppi_header.doc_version.minor >= 2) { fread(&layer->m_blend_mode, sizeof(int), 1, fp); fread(&layer->m_alpha_locked, sizeof(bool), 1, fp); fread(&layer->m_visible, sizeof(bool), 1, fp); } int frames = 1; if (ppi_header.doc_version.minor >= 3) fread(&frames, sizeof(int), 1, fp); layer->create(m_width, m_height, name.c_str()); for (int fi = 0; fi < frames; fi++) { if (fi > 0) layer->add_frame(); if (ppi_header.doc_version.minor >= 3) { int duration = layer->frame_duration(fi); fread(&duration, sizeof(int), 1, fp); } snap.clear(); for (int plane_index = 0; plane_index < 6; plane_index++) { int has_data; fread(&has_data, sizeof(int), 1, fp); snap.m_dirty_face[plane_index] = has_data; if (has_data) { int b[4]; fread(&b, sizeof(b), 1, fp); snap.m_dirty_box[plane_index] = glm::vec4(b[0], b[1], b[2], b[3]); glm::vec2 sz = zw(snap.m_dirty_box[plane_index]) - xy(snap.m_dirty_box[plane_index]); int data_size; fread(&data_size, sizeof(int), 1, fp); std::vector compressed(data_size); fread(compressed.data(), 1, data_size, fp); int imgw, imgh, imgc; uint8_t* rgba = stbi_load_from_memory(compressed.data(), data_size, &imgw, &imgh, &imgc, 4); if (rgba) { std::copy(rgba, rgba + (imgw * imgh * 4), snap.image[plane_index].get()); delete rgba; } } progress++; float p = (float)progress / total * 100.f; LOG("progress: %f", p); if (App::I->layout.m_loaded) { pb->m_progress->SetWidthP(p); } } layer->restore(snap, fi); } } std::swap(tmp_layers, m_layers); if (ppi_header.doc_version.minor >= 4) { int bytes = 0; fread(&bytes, sizeof(int), 1, fp); std::vector data(bytes); fread(data.data(), bytes, 1, fp); BinaryStreamReader sr; sr.init(data.data(), data.size(), BinaryStream::ByteOrder::LittleEndian); Serializer::Descriptor info; sr >> info; //if (info.value("has_encoder")) //{ // m_encoder = std::make_unique(); // sr >> *m_encoder; // m_encoder->init(); //} //else //{ // timelapse_reset_encoder(); //} } //else //{ // timelapse_reset_encoder(); //} fclose(fp); LOG("project restore from %s", file_path.c_str()); auto start = file_path.rfind('/') + 1; std::string file_name = file_path.substr(start, file_path.length() - start - strlen(".ppi")); std::string lapse_path = App::I->data_path + '/' + file_name + ".pptl"; if (Asset::exist(lapse_path)) { BinaryStreamReader sr; sr.load(lapse_path, BinaryStream::ByteOrder::LittleEndian); Serializer::Descriptor info; sr >> info; if (info.value("has-track-360")) { m_encoder = std::make_unique(); sr >> *m_encoder; m_encoder->init(); } } else { timelapse_reset_encoder(); } m_current_layer_idx = 0; m_current_stroke = nullptr; m_dual_stroke = nullptr; m_show_tmp = false; m_smask_active = false; m_smask_mode = 0; m_dirty = false; m_commit_delayed = false; m_dirty_stroke = false; memset(m_dirty_face, 0, sizeof(bool) * 6); memset(m_pick_ready, 0, sizeof(bool) * 6); m_unsaved = false; m_newdoc = false; if (App::I->layout.m_loaded) { pb->destroy(); App::I->ui_task([] { App::I->title_update(); App::I->update_rec_frames(); Canvas::I->anim_update(); App::I->animation->load_layers(); }); } return true; } Image Canvas::thumbnail_generate(int w, int h) { Image image; image.create(w, h); App::I->render_task([this, w, h, &image] { // save viewport and clear color states GLint vp[4]; GLfloat cc[4]; glGetIntegerv(GL_VIEWPORT, vp); glGetFloatv(GL_COLOR_CLEAR_VALUE, cc); GLboolean blend = glIsEnabled(GL_BLEND); // prepare common states glViewport(0, 0, w, h); RTT fb; fb.create(w, h); fb.bindFramebuffer(); Plane m_face_plane; m_face_plane.create<1>(2, 2); Texture2D blendtex; blendtex.create(w, h); // recalculate because of different aspect ratio than the m_proj matrix glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)w / (float)h, 0.1f, 1000.f); fb.clear({ 1, 1, 1, 0 }); for (int i = 0; i < 6; i++) { glDisable(GL_BLEND); auto plane_mvp = proj * m_mv * m_plane_transform[i] * glm::translate(glm::vec3(0, 0, -1)); ShaderManager::use(kShader::TextureBlend); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp); if (!ShaderManager::ext_framebuffer_fetch) { ShaderManager::u_int(kShaderUniform::TexBG, 2); glActiveTexture(GL_TEXTURE2); blendtex.bind(); m_sampler_nearest.bind(2); } m_sampler_nearest.bind(0); // nearest for (int layer_index = 0; layer_index < m_layers.size(); layer_index++) { if (!m_layers[layer_index]->m_visible || m_layers[layer_index]->m_opacity == 0.f || !m_layers[layer_index]->face(i)) continue; if (!ShaderManager::ext_framebuffer_fetch) { glActiveTexture(GL_TEXTURE2); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h); } ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode); ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity); glActiveTexture(GL_TEXTURE0); m_layers[layer_index]->rtt(i).bindTexture(); m_face_plane.draw_fill(); m_layers[layer_index]->rtt(i).unbindTexture(); } if (!ShaderManager::ext_framebuffer_fetch) { glActiveTexture(GL_TEXTURE2); blendtex.unbind(); } glActiveTexture(GL_TEXTURE0); blendtex.bind(); // copy the content of the fb before drawing the grid glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h); // draw the grid ShaderManager::use(kShader::Checkerboard); ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp); m_face_plane.draw_fill(); // now blend with the background glEnable(GL_BLEND); ShaderManager::use(kShader::Texture); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f)); m_sampler.bind(0); // linear m_plane.draw_fill(); blendtex.unbind(); } fb.unbindFramebuffer(); // read the rendered image fb.readTextureData((uint8_t*)image.data()); fb.destroy(); blendtex.destroy(); // restore viewport and clear color states blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND); glViewport(vp[0], vp[1], vp[2], vp[3]); glClearColor(cc[0], cc[1], cc[2], cc[3]); glActiveTexture(GL_TEXTURE0); }); return image; } Image Canvas::thumbnail_read(std::string file_path) { // static char name[128]; // sprintf(name, "%s/latlong.ppi", data_path.c_str()); FILE* fp = fopen(file_path.c_str(), "rb"); if (!fp) { LOG("cannot read project %s", file_path.c_str()); return {}; // return empty image } PPIHeader ppi_header; fread(&ppi_header, sizeof(PPIHeader), 1, fp); if (!ppi_header.valid()) return {}; Image thumb; thumb.width = ppi_header.thumb_header.width; thumb.height = ppi_header.thumb_header.height; thumb.comp = ppi_header.thumb_header.comp; thumb.create(); fread((uint8_t*)thumb.data(), thumb.size(), 1, fp); fclose(fp); LOG("project thumbnail read from %s", file_path.c_str()); return thumb; } void Canvas::draw_objects_direct(std::function observer, Layer& layer, int frame) { App::I->render_task([&] { // save viewport and clear color states GLint vp[4]; GLfloat cc[4]; glGetIntegerv(GL_VIEWPORT, vp); glGetFloatv(GL_COLOR_CLEAR_VALUE, cc); GLboolean blend = glIsEnabled(GL_BLEND); // prepare common states glViewport(0, 0, layer.w, layer.h); glDisable(GL_BLEND); GLuint rboID; glGenRenderbuffers(1, &rboID); glBindRenderbuffer(GL_RENDERBUFFER, rboID); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, layer.w, layer.h); glBindRenderbuffer(GL_RENDERBUFFER, 0); glm::mat4 proj = glm::perspective(glm::radians(90.f), 1.f, .01f, 1000.f); for (int i = 0; i < 6; i++) { glm::mat4 plane_camera = glm::lookAt(glm::vec3(0), m_plane_origin[i], m_plane_tangent[i]); layer.rtt(i, frame).bindFramebuffer(); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboID); observer(plane_camera, proj, i); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, 0); layer.rtt(i, frame).unbindFramebuffer(); layer.face(i, frame) = true; layer.box(i, frame) = { 0, 0, layer.w, layer.h }; } glDeleteRenderbuffers(1, &rboID); // restore viewport and clear color states blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND); glViewport(vp[0], vp[1], vp[2], vp[3]); glClearColor(cc[0], cc[1], cc[2], cc[3]); glActiveTexture(GL_TEXTURE0); }); } void Canvas::draw_objects(std::function observer, Layer& layer, int frame, bool save_history) { App::I->render_task([&] { // save viewport and clear color states GLint vp[4]; GLfloat cc[4]; glGetIntegerv(GL_VIEWPORT, vp); glGetFloatv(GL_COLOR_CLEAR_VALUE, cc); GLboolean blend = glIsEnabled(GL_BLEND); // prepare common states glViewport(0, 0, layer.w, layer.h); glDisable(GL_BLEND); GLuint rboID; glGenRenderbuffers(1, &rboID); glBindRenderbuffer(GL_RENDERBUFFER, rboID); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, layer.w, layer.h); glBindRenderbuffer(GL_RENDERBUFFER, 0); RTT rtt; rtt.create(layer.w, layer.h); rtt.bindFramebuffer(); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboID); rtt.unbindFramebuffer(); // allocate action to add to history ActionStroke* action; if (save_history) { action = new ActionStroke; action->was_saved = !m_unsaved; } glm::mat4 proj = glm::perspective(glm::radians(90.f), 1.f, .01f, 1000.f); for (int i = 0; i < 6; i++) { glm::mat4 plane_camera = glm::lookAt(glm::vec3(0), m_plane_origin[i], m_plane_tangent[i]); rtt.bindFramebuffer(); rtt.clear({ 1, 1, 1, 0 }); observer(plane_camera, proj, i); rtt.unbindFramebuffer(); glm::vec4 bounds = rtt.calc_bounds(); layer.rtt(i, frame).bindFramebuffer(); glm::vec2 box_sz = zw(bounds) - xy(bounds); bool has_data = box_sz.x > 0 && box_sz.y > 0; if (save_history) { // save image before commit if (has_data) { action->m_image[i] = std::make_unique(box_sz.x * box_sz.y * 4); glReadPixels(bounds.x, bounds.y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get()); action->m_box[i] = bounds; } action->m_old_box[i] = layer.box(i, frame); action->m_old_dirty[i] = layer.face(i, frame); } // draw the tmp layer into the actual layer if (has_data) { ShaderManager::use(kShader::Texture); ShaderManager::u_int(kShaderUniform::Tex, 0); ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-0.5f, 0.5f, -0.5f, 0.5f)); glActiveTexture(GL_TEXTURE0); m_sampler_nearest.bind(0); rtt.bindTexture(); m_plane.draw_fill(); rtt.unbindTexture(); layer.face(i, frame) = true; layer.box(i, frame) = { glm::min(xy(layer.box(i, frame)), xy(bounds)), glm::max(zw(layer.box(i, frame)), zw(bounds)) }; } layer.rtt(i, frame).unbindFramebuffer(); } if (save_history) { // save history action->m_layer_idx = m_current_layer_idx; action->m_frame_idx = frame; action->m_canvas = this; //action->m_stroke = std::move(m_current_stroke); ActionManager::add(action); } glDeleteRenderbuffers(1, &rboID); rtt.destroy(); // restore viewport and clear color states blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND); glViewport(vp[0], vp[1], vp[2], vp[3]); glClearColor(cc[0], cc[1], cc[2], cc[3]); glActiveTexture(GL_TEXTURE0); }); } void Canvas::draw_objects(std::function observer, int frame, bool save_history) { draw_objects(observer, layer(), frame, save_history); } void Canvas::project2Dpoints(std::vector& vertices) { for (auto& p : vertices) { glm::vec3 ro, rd, hit_o, hit_d; glm::vec2 hit_fb; int plane_id; if (point_trace(p.pos, ro, rd, hit_o, hit_fb, hit_d, plane_id)) p.pos = glm::vec4(hit_o, 1); } } glm::vec3 Canvas::project2Dpoint(glm::vec2 pt) { glm::vec3 ro, rd, hit_o, hit_d; glm::vec2 hit_fb; int plane_id; if (point_trace(pt, ro, rd, hit_o, hit_fb, hit_d, plane_id)) return glm::vec4(hit_o, 1); return glm::vec3(0); } // return the 2d shape of the faces based on the current camera // this can be used for screen space shapes clipping std::vector Canvas::face_to_shape2D(int plane_index) { static std::array corners{ glm::vec4(-1.f, +1.f, -1.f, 1.f), // A top-left glm::vec4(+1.f, +1.f, -1.f, 1.f), // B top-right glm::vec4(+1.f, -1.f, -1.f, 1.f), // C bottom-right glm::vec4(-1.f, -1.f, -1.f, 1.f), // D bottom-left }; // compute points in camera space std::vector pt_cam; for (auto c : corners) { auto pt_world = m_plane_transform[plane_index] * c; pt_cam.push_back(m_mv * pt_world); } // clip at near plane pt_cam = poly_clip_near(pt_cam, 0.01); // compute windows space std::vector points; for (auto p : pt_cam) { auto pt_clip = m_proj * glm::vec4(p, 1); pt_clip = pt_clip / pt_clip.w; glm::vec2 pt_screen = (glm::vec2(pt_clip) * 0.5f + 0.5f) * zw(m_vp); pt_screen.y = m_vp.w - pt_screen.y - 1; points.push_back(pt_screen); } return points; } void Canvas::push_camera() { m_camera_stack.push(get_camera()); } void Canvas::pop_camera() { if (!m_camera_stack.empty()) { set_camera(m_camera_stack.top()); m_camera_stack.pop(); } } CameraData Canvas::get_camera() { CameraData c; c.m_box = m_box; c.m_mv = m_mv; c.m_pan = m_pan; std::copy_n(m_plane_dir, 6, c.m_plane_dir); std::copy_n(m_plane_unproject, 6, c.m_plane_unproject); c.m_proj = m_proj; c.m_vp = m_vp; return c; } void Canvas::set_camera(const CameraData& c) { m_box = c.m_box; m_mv = c.m_mv; m_pan = c.m_pan; std::copy_n(c.m_plane_dir, 6, m_plane_dir); std::copy_n(c.m_plane_unproject, 6, m_plane_unproject); m_proj = c.m_proj; m_vp = c.m_vp; } void Canvas::timelapse_reset_encoder() noexcept { m_encoder = std::make_unique(); int res = glm::min(m_width / 2, 1024); m_encoder->init(res * 4, res * 2, 30, 2000 << 10); }