From 74efa7aa8627332f6a42f6ea4b5592eee47db625 Mon Sep 17 00:00:00 2001 From: Bart Janssens Date: Thu, 29 Sep 2016 11:21:58 +0200 Subject: [PATCH] First working GLVisualize example --- REQUIRE | 2 +- deps/src/qmlwrap/CMakeLists.txt | 2 + deps/src/qmlwrap/glvisualize_viewport.cpp | 69 ++++++++++ deps/src/qmlwrap/glvisualize_viewport.hpp | 29 +++++ deps/src/qmlwrap/julia_api.cpp | 2 +- deps/src/qmlwrap/opengl_viewport.cpp | 35 +++--- deps/src/qmlwrap/opengl_viewport.hpp | 14 ++- deps/src/qmlwrap/wrap_qml.cpp | 4 +- example/drag.jl | 5 + example/glvisualize.jl | 54 ++++++-- example/qml/drag.qml | 67 ++++++++++ example/qml/glvisualize.qml | 4 +- src/QML.jl | 2 + src/glvisualize_callbacks.jl | 145 ++++++++++++++++++++++ 14 files changed, 398 insertions(+), 36 deletions(-) create mode 100644 deps/src/qmlwrap/glvisualize_viewport.cpp create mode 100644 deps/src/qmlwrap/glvisualize_viewport.hpp create mode 100644 example/drag.jl create mode 100644 example/qml/drag.qml create mode 100644 src/glvisualize_callbacks.jl diff --git a/REQUIRE b/REQUIRE index eed5df8..7421149 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,3 +1,3 @@ julia 0.4 BinDeps -CxxWrap 0.1.4 +CxxWrap 0.1.6+ diff --git a/deps/src/qmlwrap/CMakeLists.txt b/deps/src/qmlwrap/CMakeLists.txt index b23cab3..c176cf9 100644 --- a/deps/src/qmlwrap/CMakeLists.txt +++ b/deps/src/qmlwrap/CMakeLists.txt @@ -31,6 +31,8 @@ if(WIN32) endif(WIN32) add_library(qmlwrap SHARED + glvisualize_viewport.hpp + glvisualize_viewport.cpp julia_api.hpp julia_api.cpp julia_display.hpp diff --git a/deps/src/qmlwrap/glvisualize_viewport.cpp b/deps/src/qmlwrap/glvisualize_viewport.cpp new file mode 100644 index 0000000..df1c657 --- /dev/null +++ b/deps/src/qmlwrap/glvisualize_viewport.cpp @@ -0,0 +1,69 @@ +#include + +#include "glvisualize_viewport.hpp" +#include "julia_api.hpp" + +#include +#include + +namespace qmlwrap +{ + +GLVisualizeViewport::GLVisualizeViewport(QQuickItem *parent) : OpenGLViewport(parent) +{ + QObject::connect(this, &QQuickItem::windowChanged, [this] (QQuickWindow* w) + { + if(w == nullptr && m_state != nullptr) + { + cxx_wrap::julia_call(cxx_wrap::julia_function("on_window_close", "QML"), m_state); + } + + if(w == nullptr) + { + return; + } + + connect(w, &QQuickWindow::openglContextCreated, [this] (QOpenGLContext* context) + { + connect(context, &QOpenGLContext::aboutToBeDestroyed, [] () + { + jl_function_t* on_context_destroy = cxx_wrap::julia_function("on_context_destroy", "QML"); + cxx_wrap::julia_call(on_context_destroy); + }); + }); + }); +} + +GLVisualizeViewport::~GLVisualizeViewport() +{ + if(m_state != nullptr) + { + cxx_wrap::unprotect_from_gc(m_state); + } +} + +void GLVisualizeViewport::componentComplete() +{ + OpenGLViewport::componentComplete(); + jl_function_t* sigs_ctor = cxx_wrap::julia_function("initialize_signals", "QML"); + assert(sigs_ctor != nullptr); + m_state = cxx_wrap::julia_call(sigs_ctor); + cxx_wrap::protect_from_gc(m_state); + assert(m_state != nullptr); + + auto win_size_changed = [this] () { cxx_wrap::julia_call(cxx_wrap::julia_function("on_window_size_change", "QML"), m_state, width(), height()); }; + QObject::connect(this, &QQuickItem::widthChanged, win_size_changed); + QObject::connect(this, &QQuickItem::heightChanged, win_size_changed); +} + +void GLVisualizeViewport::setup_buffer(GLuint handle, int width, int height) +{ + cxx_wrap::julia_call(cxx_wrap::julia_function("on_framebuffer_setup", "QML"), m_state, handle, static_cast(width), static_cast(height)); +} + +void GLVisualizeViewport::post_render() +{ + cxx_wrap::julia_call(cxx_wrap::julia_function("render_glvisualize_scene", "QML"), m_state); +} + +} // namespace qmlwrap diff --git a/deps/src/qmlwrap/glvisualize_viewport.hpp b/deps/src/qmlwrap/glvisualize_viewport.hpp new file mode 100644 index 0000000..9a1a846 --- /dev/null +++ b/deps/src/qmlwrap/glvisualize_viewport.hpp @@ -0,0 +1,29 @@ +#ifndef QML_glvisualize_viewport_H +#define QML_glvisualize_viewport_H + +#include + +#include "opengl_viewport.hpp" + +namespace qmlwrap +{ + +/// Multimedia display for Julia +class GLVisualizeViewport : public OpenGLViewport +{ + Q_OBJECT +public: + GLVisualizeViewport(QQuickItem* parent = 0); + virtual ~GLVisualizeViewport(); + virtual void componentComplete(); + +private: + // Julia type holding the signals and other state needed for GLVisualize. Manipulated from within Julia callbacks + jl_value_t* m_state = nullptr; + virtual void setup_buffer(GLuint handle, int width, int height); + virtual void post_render(); +}; + +} // namespace qmlwrap + +#endif diff --git a/deps/src/qmlwrap/julia_api.cpp b/deps/src/qmlwrap/julia_api.cpp index a2d633a..dc83750 100644 --- a/deps/src/qmlwrap/julia_api.cpp +++ b/deps/src/qmlwrap/julia_api.cpp @@ -32,7 +32,7 @@ QVariant JuliaAPI::call(const QString& fname, const QVariantList& args) // Process arguments for(int i = 0; i != nb_args; ++i) { - julia_args[i] = cxx_wrap::convert_to_julia(args.at(i)); + julia_args[i] = cxx_wrap::convert_to_julia(args.at(i)); if(julia_args[i] == nullptr) { qWarning() << "Julia argument type for function " << fname << " is unsupported:" << args[i].typeName(); diff --git a/deps/src/qmlwrap/opengl_viewport.cpp b/deps/src/qmlwrap/opengl_viewport.cpp index 059655d..6b613e8 100644 --- a/deps/src/qmlwrap/opengl_viewport.cpp +++ b/deps/src/qmlwrap/opengl_viewport.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -17,7 +18,14 @@ class OpenGLViewport::JuliaRenderer : public QQuickFramebufferObject::Renderer void render() { + if(m_need_setup) + { + m_vp->setup_buffer(m_handle, m_width, m_height); + m_need_setup = false; + } m_vp->render(); + m_vp->post_render(); + m_vp->window()->resetOpenGLState(); } void synchronize(QQuickFramebufferObject *item) @@ -28,19 +36,29 @@ class OpenGLViewport::JuliaRenderer : public QQuickFramebufferObject::Renderer QOpenGLFramebufferObject *createFramebufferObject(const QSize &size) { + m_need_setup = true; + m_width = size.width(); + m_height = size.height(); QOpenGLFramebufferObjectFormat format; format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); format.setSamples(4); - return new QOpenGLFramebufferObject(size, format); + auto result = new QOpenGLFramebufferObject(size, format); + m_handle = result->handle(); + return result; } private: OpenGLViewport* m_vp; + bool m_need_setup = true; + int m_width = 0; + int m_height = 0; + GLuint m_handle = 0; }; OpenGLViewport::OpenGLViewport(QQuickItem *parent) : QQuickFramebufferObject(parent) { QObject::connect(this, &OpenGLViewport::renderFunctionChanged, this, &OpenGLViewport::update); QObject::connect(this, &OpenGLViewport::renderArgumentsChanged, this, &OpenGLViewport::update); + setMirrorVertically(true); } void OpenGLViewport::render() @@ -53,19 +71,4 @@ QQuickFramebufferObject::Renderer* OpenGLViewport::createRenderer() const return new JuliaRenderer(); } -QSGNode* OpenGLViewport::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *nodeData) -{ - // This is needed to prevent the image from being upside-down - if(!node) - { - node = QQuickFramebufferObject::updatePaintNode(node, nodeData); - QSGSimpleTextureNode *n = static_cast(node); - if(n) - n->setTextureCoordinatesTransform(QSGSimpleTextureNode::MirrorVertically); - return node; - } - return QQuickFramebufferObject::updatePaintNode(node, nodeData); -} - - } // namespace qmlwrap diff --git a/deps/src/qmlwrap/opengl_viewport.hpp b/deps/src/qmlwrap/opengl_viewport.hpp index 84ecbe0..4d579b1 100644 --- a/deps/src/qmlwrap/opengl_viewport.hpp +++ b/deps/src/qmlwrap/opengl_viewport.hpp @@ -4,6 +4,7 @@ #include #include +#include #include namespace qmlwrap @@ -46,10 +47,17 @@ class OpenGLViewport : public QQuickFramebufferObject void renderFunctionChanged(QString); void renderArgumentsChanged(QVariantList); -protected: - QSGNode* updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *nodeData); - private: + /// Hook to do extra setup the first time an FBO is used. The FBO is called in render, i.e. when the FBO is bound + virtual void setup_buffer(GLuint handle, int width, int height) + { + } + + /// Called after the user-defined rendering function + virtual void post_render() + { + } + QString m_render_function; QVariantList m_render_arguments; Q_INVOKABLE void render(); diff --git a/deps/src/qmlwrap/wrap_qml.cpp b/deps/src/qmlwrap/wrap_qml.cpp index 8bd1414..fd4682f 100644 --- a/deps/src/qmlwrap/wrap_qml.cpp +++ b/deps/src/qmlwrap/wrap_qml.cpp @@ -14,6 +14,7 @@ #include "julia_object.hpp" #include "julia_signals.hpp" #include "opengl_viewport.hpp" +#include "glvisualize_viewport.hpp" #include "type_conversion.hpp" namespace qmlwrap @@ -259,6 +260,7 @@ JULIA_CPP_MODULE_BEGIN(registry) qmlRegisterType("org.julialang", 1, 0, "JuliaSignals"); qmlRegisterType("org.julialang", 1, 0, "JuliaDisplay"); qmlRegisterType("org.julialang", 1, 0, "OpenGLViewport"); + qmlRegisterType("org.julialang", 1, 0, "GLVisualizeViewport"); qml_module.add_abstract("QObject"); @@ -320,7 +322,7 @@ JULIA_CPP_MODULE_BEGIN(registry) qml_module.add_type("JuliaObject", julia_type()) .method("set", &qmlwrap::JuliaObject::set) // Not exported, use @qmlset - .method("value", &qmlwrap::JuliaObject::value); // Not exported, use @qmlget + .method("julia_object_value", &qmlwrap::JuliaObject::value); // Not exported, use @qmlget // Emit signals helper qml_module.method("emit", [](const char* signal_name, cxx_wrap::ArrayRef args) diff --git a/example/drag.jl b/example/drag.jl new file mode 100644 index 0000000..36a25dc --- /dev/null +++ b/example/drag.jl @@ -0,0 +1,5 @@ +using QML + +@qmlfunction println +@qmlapp joinpath(dirname(@__FILE__), "qml", "drag.qml") +exec() diff --git a/example/glvisualize.jl b/example/glvisualize.jl index 35864b0..6e88382 100644 --- a/example/glvisualize.jl +++ b/example/glvisualize.jl @@ -3,24 +3,54 @@ ENV["QSG_RENDER_LOOP"] = "basic" using QML using GLVisualize, GeometryTypes, GLAbstraction, Colors +using Reactive -function render(t) - #timesignal = bounce(linspace(0.0, 1.0, 360)) - const N = 2048 - function spiral(i, start_radius, offset) - Point2f0(sin(i), cos(i)) * (start_radius + ((i/2pi)*offset)) +# lines2d example +const N = 2048 +function spiral(i, start_radius, offset) + Point2f0(sin(i), cos(i)) * (start_radius + ((i/2pi)*offset)) +end +# 2D particles +curve_data(i, N) = Point2f0[spiral(i+x/20f0, 1, (i/20)+1) for x=1:N] + +timesignal = Signal(0.) + +t = const_lift(x-> (1f0-x)*100f0, timesignal) +color = map(RGBA{Float32}, colormap("Blues", N)) + +# The qml_time parameter is a Float64 from the QML slider +function render(qml_time) + global robj + if(!isdefined(:robj)) + robj = visualize(const_lift(curve_data, t, N), :lines, color=color) end - # 2D particles - curve_data(i, N) = Point2f0[spiral(i+x/20f0, 1, (i/20)+1) for x=1:N] + push!(timesignal, qml_time) - # t = const_lift(x-> (1f0-x)*100f0, timesignal) - color = map(RGBA{Float32}, colormap("Blues", N)) - #view(visualize(const_lift(curve_data, t, N), :lines, color=color)) - visualize(const_lift(curve_data, t, N), :lines, color=color) + view(robj) - #renderloop(window) + return end +# Cat example shows up blank +# mesh = loadasset("cat.obj") +# timesignal = Signal(0.) +# rotation_angle = const_lift(*, timesignal, 2f0*pi) +# start_rotation = Signal(rotationmatrix_x(deg2rad(90f0))) +# rotation = map(rotationmatrix_y, rotation_angle) +# final_rotation = map(*, start_rotation, rotation) +# +# # Render function that takes a parameter t from a QML slider +# function render(t) +# global robj +# if(!isdefined(:robj)) +# robj = visualize(mesh, model=final_rotation) +# end +# +# push!(timesignal, t) +# +# view(robj) +# end + @qmlapp joinpath(dirname(@__FILE__), "qml", "glvisualize.qml") exec() diff --git a/example/qml/drag.qml b/example/qml/drag.qml new file mode 100644 index 0000000..b93e2e0 --- /dev/null +++ b/example/qml/drag.qml @@ -0,0 +1,67 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0 +import QtQuick.Layouts 1.0 +import org.julialang 1.0 + +ApplicationWindow { + title: "My Application" + width: 800 + height: 600 + visible: true + + Rectangle { + id: container + width: 800; height: 600 + + SystemPalette { id: pal; colorGroup: SystemPalette.Active } + + Canvas { + id: paths + anchors.fill: parent + contextType: "2d" + + Path { + id: myPath + startX: 0; startY: 100 + + PathCurve { x: 75; y: 75 } + PathCurve { x: 200; y: 150 } + PathCurve { x: 325; y: 25 } + PathCurve { x: rect.x; y: rect.y } + } + + onPaint: { + context.fillStyle = pal.window; + context.fillRect(0, 0, width, height); + context.strokeStyle = Qt.rgba(.4,.6,.8); + context.path = myPath; + context.stroke(); + } + } + + Rectangle { + id: rect + width: 50; height: 50 + color: "red" + + onXChanged: paths.requestPaint() + + MouseArea { + anchors.fill: parent + drag.target: rect + } + + Rectangle { + id: conn + y: 10 + width: 10; height: 5 + color: "yellow" + + MouseArea { + anchors.fill: parent + onClicked: console.log(rect.x, ", ", rect.y) + } + } + } + } +} diff --git a/example/qml/glvisualize.qml b/example/qml/glvisualize.qml index b51d989..72366b1 100644 --- a/example/qml/glvisualize.qml +++ b/example/qml/glvisualize.qml @@ -26,11 +26,11 @@ ApplicationWindow { id: time value: 0. minimumValue: 0. - maximumValue: 360. + maximumValue: 1. } } - OpenGLViewport { + GLVisualizeViewport { id: jvp Layout.fillWidth: true Layout.fillHeight: true diff --git a/src/QML.jl b/src/QML.jl index 93f8b0a..8a94e44 100644 --- a/src/QML.jl +++ b/src/QML.jl @@ -117,6 +117,8 @@ end export @qmlget, @qmlset, @emit, @qmlfunction, @qmlapp +include("glvisualize_callbacks.jl") + @doc """ Module for building [Qt5 QML](http://doc.qt.io/qt-5/qtqml-index.html) graphical user interfaces for Julia programs. Types starting with `Q` are equivalent of their Qt C++ counterpart, so they have no Julia docstring and we refer to diff --git a/src/glvisualize_callbacks.jl b/src/glvisualize_callbacks.jl new file mode 100644 index 0000000..4c97d06 --- /dev/null +++ b/src/glvisualize_callbacks.jl @@ -0,0 +1,145 @@ +using Colors +using FixedPointNumbers +using FixedSizeArrays +using GeometryTypes +using GLAbstraction +using GLFW +using GLVisualize +using GLWindow +using ModernGL +using Reactive + +type GLVisualizeState + window_open::Signal{Bool} + window_size::Signal{Vec{2,Int}} + window_position::Signal{Vec{2,Int}} + keyboard_buttons::Signal{NTuple{4, Int}} + mouse_buttons::Signal{NTuple{3, Int}} + dropped_files::Signal{Vector{Compat.UTF8String}} + framebuffer_size::Signal{Vec{2,Int}} + unicode_input::Signal{Vector{Char}} + cursor_position::Signal{Vec{2, Float64}} + scroll::Signal{Vec{2, Float64}} + hasfocus::Signal{Bool} + entered_window::Signal{Bool} + + screen::Screen + + GLVisualizeState() = new( + Signal(true), + Signal(Vec{2,Int}(0,0)), + Signal(Vec{2,Int}(0,0)), + Signal((0,0,0,0)), + Signal((0,0,0)), + Signal(Compat.UTF8String[]), + Signal(Vec{2,Int}(0,0)), + Signal(Char[]), + Signal(Vec(0.,0.)), + Signal(Vec(0.,0.)), + Signal(false), + Signal(false) + ) +end + +function initialize_signals() + state = GLVisualizeState() + return state +end + +function on_framebuffer_setup(state, handle, width, height) + signal_dict = Dict{Symbol, Any}() + + window = GLFW.Window(C_NULL) + + push!(state.framebuffer_size, Vec2(width, height)) + + # window area signal as a rectangle + window_area = map(SimpleRectangle, + Signal(Vec(0,0)), + state.framebuffer_size + ) + signal_dict[:window_area] = window_area + + # mouse position in pixel coordinates with 0,0 in left down corner + signal_dict[:mouseposition] = state.cursor_position + + signal_dict[:mouse2id] = Signal(GLWindow.SelectionID{Int}(-1, -1)) + + color = RGBA{Float32}(1,1,1,1) + + for fdname in fieldnames(state)[1:end-1] + signal_dict[fdname] = getfield(state,fdname) + end + + buffersize = tuple(width, height) + color_buffer = GLAbstraction.Texture(RGBA{UFixed8}, buffersize, minfilter=:nearest, x_repeat=:clamp_to_edge) + objectid_buffer = Texture(Vec{2, GLushort}, buffersize, minfilter=:nearest, x_repeat=:clamp_to_edge) + depth_buffer = Texture(Float32, buffersize, + internalformat = GL_DEPTH_COMPONENT32F, + format = GL_DEPTH_COMPONENT, + minfilter=:nearest, x_repeat=:clamp_to_edge + ) + + # GLWindow.attach_framebuffer(color_buffer, GL_COLOR_ATTACHMENT0) + # GLWindow.attach_framebuffer(objectid_buffer, GL_COLOR_ATTACHMENT1) + # GLWindow.attach_framebuffer(depth_buffer, GL_DEPTH_ATTACHMENT) + + p = GLWindow.postprocess(color_buffer, state.framebuffer_size) + fb = GLWindow.GLFramebuffer(handle, color_buffer, objectid_buffer, depth_buffer, p) + + if !isdefined(state, :screen) + state.screen = Screen(Symbol("QMLWindow"), + window_area, Screen[], signal_dict, + (), false, color, + Dict{Symbol, Any}(), + GLWindow.GLContext(window, fb) + ) + else + state.screen.glcontext = GLWindow.GLContext(window, fb) + end + + GLVisualize.set_root_screen(state.screen) + GLWindow.add_complex_signals!(state.screen) +end + +on_window_close(signals) = push!(signals.window_open, false) +on_window_size_change(signals, w, h) = push!(signals.window_size, Vec{2,Int}(w, h)) + +function on_context_destroy() + println("on_context_destroy called") + GLAbstraction.empty_shader_cache!() + return +end + +# Copy of the render function from GLWindow, Screen should be abstract so isopen and ishidden can be overridden +function qml_render(x::Screen, parent::Screen=x, context=x.area.value) + sa = value(x.area) + sa = SimpleRectangle(context.x+sa.x, context.y+sa.y, sa.w, sa.h) # bring back to absolute values + pa = context + sa_pa = intersect(pa, sa) # intersection with parent + if sa_pa != SimpleRectangle{Int}(0,0,0,0) # if it is in the parent area + #glEnable(GL_SCISSOR_TEST) + #glScissor(sa_pa) + #glViewport(sa) + colorbits = GL_DEPTH_BUFFER_BIT + if alpha(x.color) > 0 + glClearColor(red(x.color), green(x.color), blue(x.color), alpha(x.color)) + colorbits = colorbits | GL_COLOR_BUFFER_BIT + end + glClear(colorbits) + + render(x.renderlist) + for window in x.children + render(window, x, sa) + end + end +end + +function render_glvisualize_scene(state) + fb = GLWindow.framebuffer(state.screen) + GLWindow.prepare(fb) + qml_render(state.screen) + GLWindow.push_selectionqueries!(state.screen) + GLWindow.display(fb, state.screen) + #GLWindow.swapbuffers(state.screen) +end