diff --git a/examples/simple_demo_qml/CMakeLists.txt b/examples/simple_demo_qml/CMakeLists.txt new file mode 100644 index 000000000..a4dcf597e --- /dev/null +++ b/examples/simple_demo_qml/CMakeLists.txt @@ -0,0 +1,92 @@ +cmake_minimum_required(VERSION 3.10.2 FATAL_ERROR) +project(ignition-rendering-simple-demo-qml) + +#------------------------------------------------------------------------ +# Compile as C++14 + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +#------------------------------------------------------------------------ +# Find ignition-cmake +find_package(ignition-cmake2 2.3 REQUIRED) +set(IGN_CMAKE_VER ${ignition-cmake2_VERSION_MAJOR}) + +#------------------------------------------------------------------------ +# Find ign-rendering +find_package(ignition-rendering6) + +#====================================== +# Find Qt +find_package(Qt5 REQUIRED COMPONENTS + Core + Gui + Qml + Quick + QuickControls2 + Widgets +) + +include_directories( + ${PROJECT_SOURCE_DIR}/include + ${Qt5Core_INCLUDE_DIRS} + ${Qt5Qml_INCLUDE_DIRS} + ${Qt5Quick_INCLUDE_DIRS} + ${Qt5QuickControls2_INCLUDE_DIRS} + ${Qt5Widgets_INCLUDE_DIRS} +) + +add_definitions( + ${Qt5Widgets_DEFINITIONS} + ${Qt5Qml_DEFINITIONS} + ${${Qt5Quick_DEFINITIONS}} +) + +#====================================== +# Configure Qt + +qt5_add_resources(QT_RESOURCES + Main.qrc +) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS}") +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +#====================================== +# Build + +add_executable(simple_demo_qml + Main.cc + IgnitionRenderer.hh + IgnitionRenderer.cc + ThreadRenderer.h + ThreadRenderer.cpp + ${QT_RESOURCES} +) + +target_include_directories(simple_demo_qml PUBLIC + ${IGNITION-RENDERING_INCLUDE_DIRS} + ${OGRE2_INCLUDE_DIRS} + ${OpenGL_INCLUDE_DIRS} +) + +target_link_libraries(simple_demo_qml PUBLIC + ${IGNITION-RENDERING_LIBRARIES} + ${OGRE2_LIBRARIES} + ${OPENGL_LIBRARIES} + Qt5::Core + Qt5::Gui + Qt5::Qml + Qt5::Quick +) + +set_target_properties(simple_demo_qml + PROPERTIES + QT_QML_MODULE_VERSION 1.0 + QT_QML_MODULE_URI IgnitionRendering + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} +) diff --git a/examples/simple_demo_qml/IgnitionRenderer.cc b/examples/simple_demo_qml/IgnitionRenderer.cc new file mode 100644 index 000000000..5b25f3dfb --- /dev/null +++ b/examples/simple_demo_qml/IgnitionRenderer.cc @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2021 Rhys Mainwaring + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// The functions BuildScene and createCamera are copied from the simple_demo +// example. + +#include "IgnitionRenderer.hh" + +#include +#include + +#include + +#include + +using namespace ignition; +using namespace rendering; + +////////////////////////////////////////////////// +void BuildScene(ignition::rendering::ScenePtr _scene) +{ + // initialize _scene + _scene->SetAmbientLight(0.3, 0.3, 0.3); + VisualPtr root = _scene->RootVisual(); + + // create directional light + DirectionalLightPtr light0 = _scene->CreateDirectionalLight(); + light0->SetDirection(-0.5, 0.5, -1); + light0->SetDiffuseColor(0.5, 0.5, 0.5); + light0->SetSpecularColor(0.5, 0.5, 0.5); + root->AddChild(light0); + + // create point light + PointLightPtr light2 = _scene->CreatePointLight(); + light2->SetDiffuseColor(0.5, 0.5, 0.5); + light2->SetSpecularColor(0.5, 0.5, 0.5); + light2->SetLocalPosition(3, 5, 5); + root->AddChild(light2); + + // create green material + MaterialPtr green = _scene->CreateMaterial(); + green->SetAmbient(0.0, 0.5, 0.0); + green->SetDiffuse(0.0, 0.7, 0.0); + green->SetSpecular(0.5, 0.5, 0.5); + green->SetShininess(50); + green->SetReflectivity(0); + + // create center visual + VisualPtr center = _scene->CreateVisual(); + center->AddGeometry(_scene->CreateSphere()); + center->SetLocalPosition(3, 0, 0); + center->SetLocalScale(0.1, 0.1, 0.1); + center->SetMaterial(green); + root->AddChild(center); + + // create red material + MaterialPtr red = _scene->CreateMaterial(); + red->SetAmbient(0.5, 0.0, 0.0); + red->SetDiffuse(1.0, 0.0, 0.0); + red->SetSpecular(0.5, 0.5, 0.5); + red->SetShininess(50); + red->SetReflectivity(0); + red->SetRenderOrder(3); + + // create sphere visual + VisualPtr sphere = _scene->CreateVisual(); + sphere->AddGeometry(_scene->CreateSphere()); + sphere->SetOrigin(0.0, -0.5, 0.0); + sphere->SetLocalPosition(3, 0, 0); + sphere->SetLocalRotation(0, 0, 0); + sphere->SetLocalScale(1, 1, 1); + sphere->SetMaterial(red); + root->AddChild(sphere); + + // create blue material + MaterialPtr blue = _scene->CreateMaterial(); + blue->SetAmbient(0.0, 0.0, 0.3); + blue->SetDiffuse(0.0, 0.0, 0.8); + blue->SetSpecular(0.5, 0.5, 0.5); + blue->SetShininess(50); + blue->SetReflectivity(0); + + // create box visual + VisualPtr box = _scene->CreateVisual(); + box->AddGeometry(_scene->CreateBox()); + box->SetOrigin(0.0, 0.5, 0.0); + box->SetLocalPosition(3, 0, 0); + box->SetLocalRotation(IGN_PI / 4, 0, IGN_PI / 3); + box->SetLocalScale(1, 2.5, 1); + box->SetMaterial(blue); + root->AddChild(box); + + // create ellipsoid visual + VisualPtr ellipsoidVisual = _scene->CreateVisual(); + auto ellipsoid = _scene->CreateSphere(); + ellipsoidVisual->SetLocalScale(1.2, 0.7, 0.5); + ellipsoidVisual->AddGeometry(ellipsoid); + ellipsoidVisual->SetLocalPosition(3, -1, 0); + ellipsoidVisual->SetMaterial(green); + root->AddChild(ellipsoidVisual); + + // create white material + MaterialPtr white = _scene->CreateMaterial(); + white->SetAmbient(0.5, 0.5, 0.5); + white->SetDiffuse(0.8, 0.8, 0.8); + white->SetReceiveShadows(true); + white->SetReflectivity(0); + white->SetRenderOrder(0); + + VisualPtr capsuleVisual = _scene->CreateVisual(); + CapsulePtr capsule = _scene->CreateCapsule(); + capsule->SetLength(0.2); + capsule->SetRadius(0.2); + capsuleVisual->AddGeometry(capsule); + capsuleVisual->SetOrigin(0.0, 0.0, 0.0); + capsuleVisual->SetLocalPosition(4, 2, 0); + capsuleVisual->SetLocalScale(1, 1, 1); + capsuleVisual->SetMaterial(red); + root->AddChild(capsuleVisual); + + // create plane visual + VisualPtr plane = _scene->CreateVisual(); + plane->AddGeometry(_scene->CreatePlane()); + plane->SetLocalScale(5, 8, 1); + plane->SetLocalPosition(3, 0, -0.5); + plane->SetMaterial(white); + root->AddChild(plane); + + // create plane visual + VisualPtr plane2 = _scene->CreateVisual(); + plane2->AddGeometry(_scene->CreatePlane()); + plane2->SetLocalScale(5, 8, 1); + plane2->SetLocalPosition(4, 0.5, -0.5); + plane2->Scale(0.1, 0.1, 1); + plane2->SetMaterial(red); + root->AddChild(plane2); + + // create axis visual + VisualPtr axis = _scene->CreateAxisVisual(); + axis->SetLocalPosition(4.0, 0.5, -0.4); + root->AddChild(axis); + + // create camera + CameraPtr camera = _scene->CreateCamera("camera"); + camera->SetLocalPosition(0.0, 0.0, 0.0); + camera->SetLocalRotation(0.0, 0.0, 0.0); + camera->SetImageWidth(800); + camera->SetImageHeight(600); + camera->SetAntiAliasing(2); + camera->SetAspectRatio(800.0/600.0); + camera->SetHFOV(IGN_PI / 2); + root->AddChild(camera); + + // track target + camera->SetTrackTarget(box); +} + +////////////////////////////////////////////////// +ignition::rendering::CameraPtr CreateCamera(const std::string &_engineName) +{ + // create and populate scene + std::map params; + + // ensure that the QML application and Ignition / Ogre2 share an OpenGL + // context + params["useCurrentGLContext"] = "1"; + RenderEngine *engine = rendering::engine(_engineName, params); + if (!engine) + { + std::cout << "Engine '" << _engineName + << "' is not supported" << std::endl; + return CameraPtr(); + } + ScenePtr scene = engine->CreateScene("scene"); + BuildScene(scene); + + // return camera sensor + SensorPtr sensor = scene->SensorByName("camera"); + return std::dynamic_pointer_cast(sensor); +} + +////////////////////////////////////////////////// +IgnitionRenderer::~IgnitionRenderer() +{ +} + +////////////////////////////////////////////////// +IgnitionRenderer::IgnitionRenderer() +{ +} + +////////////////////////////////////////////////// +void IgnitionRenderer::Initialise() +{ + // no-op - all initialised on the main thread +} + +////////////////////////////////////////////////// +void IgnitionRenderer::InitialiseOnMainThread() +{ + if (!this->initialised) + { + this->InitEngine(); + this->initialised = true; + } +} + +////////////////////////////////////////////////// +void IgnitionRenderer::Render() +{ + // pre-render may regenerate textureId if the size changes + this->camera->PreRender(); + this->textureId = this->camera->RenderTextureGLId(); + + // render to texture + this->camera->Update(); + + // Move camera + this->UpdateCamera(); +} + +////////////////////////////////////////////////// +bool IgnitionRenderer::Initialised() const +{ + return this->initialised; +} + +////////////////////////////////////////////////// +unsigned int IgnitionRenderer::TextureId() const +{ + return this->textureId; +} + +////////////////////////////////////////////////// +QSize IgnitionRenderer::TextureSize() const +{ + return this->textureSize; +} + +////////////////////////////////////////////////// +void IgnitionRenderer::InitEngine() +{ + std::string engineName("ogre2"); + + common::Console::SetVerbosity(4); + + try + { + this->camera = CreateCamera(engineName); + } + catch (...) + { + std::cerr << "Error starting up: " << engineName << std::endl; + } + + if (!this->camera) + { + ignerr << "No cameras found. Scene will not be rendered" << std::endl; + return; + } + + // quick check on sizing... + ignmsg << "imageW: " << this->camera->ImageWidth() << "\n"; + ignmsg << "imageH: " << this->camera->ImageHeight() << "\n"; + + // pre-render will force texture creation and may update texture id + this->camera->PreRender(); + this->textureId = this->camera->RenderTextureGLId(); +} + +////////////////////////////////////////////////// +void IgnitionRenderer::UpdateCamera() +{ + double angle = this->cameraOffset / 2 * M_PI; + double x = sin(angle) * 3.0 + 3.0; + double y = cos(angle) * 3.0; + this->camera->SetLocalPosition(x, y, 0.0); + + this->cameraOffset += 0.0005; +} diff --git a/examples/simple_demo_qml/IgnitionRenderer.hh b/examples/simple_demo_qml/IgnitionRenderer.hh new file mode 100644 index 000000000..6edd9172d --- /dev/null +++ b/examples/simple_demo_qml/IgnitionRenderer.hh @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 Rhys Mainwaring + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#ifndef IGNITION_RENDERING_EXAMPLES_SIMPLE_DEMO_QML_IGNITION_RENDERER_HH_ +#define IGNITION_RENDERING_EXAMPLES_SIMPLE_DEMO_QML_IGNITION_RENDERER_HH_ + +#include + +#include + +/// \brief Ignition renderer class. This manages the initialisation +/// and update of an Ignition rendering engine instance and makes the +/// rendered texture available in a shared context for an application +/// to apply to a render surface. +class IgnitionRenderer +{ + /// \brief Destructor + public: virtual ~IgnitionRenderer(); + + /// \brief Constructor + public: IgnitionRenderer(); + + /// \brief Render the next frame. May be called on a render thread. + public: void Render(); + + /// \brief Initialise the render engine and scene. May be called on a render + /// thread. + public: void Initialise(); + + /// \brief Initialise the render engine and scene. Must be called on the main + /// thread. + public: void InitialiseOnMainThread(); + + /// \brief Return a boolean: true if the renderer is initialised. + public: bool Initialised() const; + + /// \brief Return the ID of the OpenGL texture. + public: unsigned int TextureId() const; + + /// \brief Return the size of the texture + public: QSize TextureSize() const; + + /// \brief Initialise the render engine. Must be called on the main thread. + private: void InitEngine(); + + /// \brief Move the camera position one step in its orbit. + private: void UpdateCamera(); + + /// \brief The OpenGL texture ID + private: unsigned int textureId = 0; + + /// \brief The sise of the texture being rendered + private: QSize textureSize = QSize(800, 600); + + /// \brief A flag to mark if the renderer has been initialised + private: bool initialised = false; + + /// \brief The current camera offset in its orbit + private: double cameraOffset = 0.0; + + /// \brief The camera for the example scene + private: ignition::rendering::CameraPtr camera; +}; + +#endif // IGNITION_RENDERING_EXAMPLES_SIMPLE_DEMO_QML_IGNITION_RENDERER_HH_ diff --git a/examples/simple_demo_qml/Main.cc b/examples/simple_demo_qml/Main.cc new file mode 100644 index 000000000..512b260da --- /dev/null +++ b/examples/simple_demo_qml/Main.cc @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 Rhys Mainwaring + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include "ThreadRenderer.h" + +#include +#include +#include + +#include +#include + +int main(int _argc, char** _argv) +{ + try + { + // use single-threaded scene graph rendering + qputenv("QSG_RENDER_LOOP", "basic"); + + // requested surface format + QSurfaceFormat format = RenderThread::CreateSurfaceFormat(); + QSurfaceFormat::setDefaultFormat(format); + RenderThread::Print(format); + + qmlRegisterType("IgnitionRendering", 1, 0, + "ThreadRenderer"); + + QGuiApplication app(_argc, _argv); + + int execReturn = 0; + { + QQuickView view; + + // Rendering in a thread introduces a slightly more complicated cleanup + // so we ensure that no cleanup of graphics resources happen until the + // application is shutting down. + view.setPersistentOpenGLContext(true); + view.setPersistentSceneGraph(true); + + view.setResizeMode(QQuickView::SizeRootObjectToView); + view.setSource(QUrl("qrc:/Main.qml")); + view.show(); + + execReturn = app.exec(); + } + + // As the render threads make use of our QGuiApplication object + // to clean up gracefully, wait for them to finish before + // QGuiApp is taken off the heap. + for (QThread *t : qAsConst(ThreadRenderer::threads)) + { + t->wait(); + delete t; + t = nullptr; + } + + return execReturn; + } + catch (const std::exception& e) + { + std::cerr << e.what() << '\n'; + } + catch (...) + { + std::cerr << "Unknown exception" << '\n'; + } + return -1; +} diff --git a/examples/simple_demo_qml/Main.qml b/examples/simple_demo_qml/Main.qml new file mode 100644 index 000000000..6ffc0e995 --- /dev/null +++ b/examples/simple_demo_qml/Main.qml @@ -0,0 +1,35 @@ +import QtQuick 2.0 +import IgnitionRendering 1.0 + +Item { + width: 800 + height: 600 + + ThreadRenderer { + id: renderer + anchors.fill: parent + anchors.margins: 10 + opacity: 0 + Component.onCompleted: renderer.opacity = 1; + } + + Rectangle { + id: labelFrame + anchors.margins: -10 + radius: 5 + color: "white" + border.color: "black" + opacity: 0.8 + anchors.fill: label + } + + Text { + id: label + anchors.bottom: renderer.bottom + anchors.left: renderer.left + anchors.right: renderer.right + anchors.margins: 20 + wrapMode: Text.WordWrap + text: "simple_demo_qml : the `simple_demo` example using ignition-rendering and QML." + } +} diff --git a/examples/simple_demo_qml/Main.qrc b/examples/simple_demo_qml/Main.qrc new file mode 100644 index 000000000..04e6770c7 --- /dev/null +++ b/examples/simple_demo_qml/Main.qrc @@ -0,0 +1,6 @@ + + + + Main.qml + + diff --git a/examples/simple_demo_qml/ThreadRenderer.cpp b/examples/simple_demo_qml/ThreadRenderer.cpp new file mode 100644 index 000000000..36e4343fa --- /dev/null +++ b/examples/simple_demo_qml/ThreadRenderer.cpp @@ -0,0 +1,427 @@ +/* + * Copyright (C) 2021 Rhys Mainwaring + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Elements of this example are derived from the textureinthread example from the Qt Toolkit +// https://code.qt.io/cgit/qt/qtdeclarative.git/tree/examples/quick/scenegraph/textureinthread?h=5.15 + +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "ThreadRenderer.h" +#include "IgnitionRenderer.hh" + +#include +#include +#include +#include +#include +#include + +QList ThreadRenderer::threads; + +//----------------------------------------------------------------------- +RenderThread::RenderThread(const QSize &_size, QQuickItem *_renderWindowItem) + : size(_size), renderWindowItem(_renderWindowItem) +{ + ThreadRenderer::threads << this; +} + +//----------------------------------------------------------------------- +void RenderThread::Print(const QSurfaceFormat &_format) +{ + auto formatOptionsToString = [] (QSurfaceFormat::FormatOptions _value) -> std::string + { + std::string options; + + if (_value & QSurfaceFormat::StereoBuffers) + { + options.append("StereoBuffers"); + } + if (_value & QSurfaceFormat::DebugContext) + { + options.empty() ? options.append("") : options.append(", "); + options.append("DebugContext"); + } + if (_value & QSurfaceFormat::DeprecatedFunctions) + { + options.empty() ? options.append("") : options.append(", "); + options.append("DeprecatedFunctions"); + } + if (_value & QSurfaceFormat::ResetNotification) + { + options.empty() ? options.append("") : options.append(", "); + options.append("ResetNotification"); + } + + return options; + }; + + auto openGLContextProfileToString = [] (QSurfaceFormat::OpenGLContextProfile _value) -> std::string + { + switch (_value) + { + case QSurfaceFormat::NoProfile: + return "NoProfile"; + case QSurfaceFormat::CoreProfile: + return "CoreProfile"; + case QSurfaceFormat::CompatibilityProfile: + return "CompatibilityProfile"; + default: + return "Invalid OpenGLContextProfile"; + } + }; + + auto renderableTypeToString = [] (QSurfaceFormat::RenderableType _value) -> std::string + { + switch (_value) + { + case QSurfaceFormat::DefaultRenderableType: + return "DefaultRenderableType"; + case QSurfaceFormat::OpenGL: + return "OpenGL"; + case QSurfaceFormat::OpenGLES: + return "OpenGLES"; + case QSurfaceFormat::OpenVG: + return "OpenVG"; + default: + return "Invalid RenderableType"; + } + }; + + auto swapBehaviorToString = [] (QSurfaceFormat::SwapBehavior _value) -> std::string + { + switch (_value) + { + case QSurfaceFormat::DefaultSwapBehavior: + return "DefaultSwapBehavior"; + case QSurfaceFormat::SingleBuffer: + return "SingleBuffer"; + case QSurfaceFormat::DoubleBuffer: + return "DoubleBuffer"; + default: + return "Invalid SwapBehavior"; + } + }; + + // surface format info + ignmsg << "version: " + << _format.version().first << "." + << _format.version().second << "\n"; + ignmsg << "profile: " + << openGLContextProfileToString(_format.profile()) << "\n"; + ignmsg << "options: " + << formatOptionsToString(_format.options()) << "\n"; + ignmsg << "renderableType: " + << renderableTypeToString(_format.renderableType()) << "\n"; + ignmsg << "hasAlpha: " << _format.hasAlpha() << "\n"; + ignmsg << "redBufferSize: " << _format.redBufferSize() << "\n"; + ignmsg << "greenBufferSize: " << _format.greenBufferSize() << "\n"; + ignmsg << "blueBufferSize: " << _format.blueBufferSize() << "\n"; + ignmsg << "alphaBufferSize: " << _format.alphaBufferSize() << "\n"; + ignmsg << "depthBufferSize: " << _format.depthBufferSize() << "\n"; + ignmsg << "stencilBufferSize: " << _format.stencilBufferSize() << "\n"; + ignmsg << "samples: " << _format.samples() << "\n"; + ignmsg << "swapBehavior: " + << swapBehaviorToString(_format.swapBehavior()) << "\n"; + ignmsg << "swapInterval: " << _format.swapInterval() << "\n"; + ignmsg << "\n"; +} + +//----------------------------------------------------------------------- +QSurfaceFormat RenderThread::CreateSurfaceFormat() +{ + // QSurfaceFormat format; + QSurfaceFormat format(QSurfaceFormat::DeprecatedFunctions); + format.setDepthBufferSize(24); + format.setStencilBufferSize(8); + format.setMajorVersion(4); + format.setMinorVersion(1); + format.setProfile(QSurfaceFormat::CoreProfile); + format.setRenderableType(QSurfaceFormat::OpenGL); + + return format; +} + +//-------------------------------------------------------------------------- +// called when the render node emits textureInUse +void RenderThread::InitialiseOnMainThread() +{ + this->context->makeCurrent(this->surface); + Print(this->context->format()); + + // create renderer + this->renderer = new IgnitionRenderer(); + this->renderer->InitialiseOnMainThread(); + + this->context->doneCurrent(); +} + +//-------------------------------------------------------------------------- +// called when the render node emits textureInUse +void RenderThread::RenderNext() +{ + this->context->makeCurrent(this->surface); + + if (!this->renderer->Initialised()) + { + this->renderer->Initialise(); + } + + // check if engine has been successfully initialized + if (!this->renderer->Initialised()) + { + ignerr << "Unable to initialize renderer" << std::endl; + return; + } + + this->renderer->Render(); + + emit this->TextureReady(this->renderer->TextureId(), this->renderer->TextureSize()); + + this->context->doneCurrent(); +} + +//-------------------------------------------------------------------------- +void RenderThread::ShutDown() +{ + this->context->makeCurrent(this->surface); + + delete this->renderer; + this->renderer = nullptr; + + this->context->doneCurrent(); + + delete this->context; + this->context = nullptr; + + // schedule this to be deleted only after we're done cleaning up + this->surface->deleteLater(); + + // Stop event processing, move the thread to GUI and make sure it is deleted. + exit(); + moveToThread(QGuiApplication::instance()->thread()); +} + +//-------------------------------------------------------------------------- +//-------------------------------------------------------------------------- +TextureNode::TextureNode(QQuickWindow *_window) + : id(0) + , size(0, 0) + , texture(nullptr) + , window(_window) +{ + // Our texture node must have a texture, so use the default 0 texture. + // createTextureFromNativeObject() + this->texture = this->window->createTextureFromId(0, QSize(1, 1)); + this->setTexture(this->texture); + this->setFiltering(QSGTexture::Linear); +} + +//-------------------------------------------------------------------------- +TextureNode::~TextureNode() +{ + delete this->texture; + this->texture = nullptr; +} + +//-------------------------------------------------------------------------- +// called when RenderThread emits signal textureReady +void TextureNode::NewTexture(int _id, const QSize &_size) +{ + this->mutex.lock(); + this->id = _id; + this->size = _size; + this->mutex.unlock(); + + // We cannot call QQuickWindow::update directly here, as this is only allowed + // from the rendering thread or GUI thread. + emit this->PendingNewTexture(); +} + +//-------------------------------------------------------------------------- +// called when the window emits beforeRendering +void TextureNode::PrepareNode() +{ + this->mutex.lock(); + int newId = this->id; + QSize size = this->size; + this->id = 0; + this->mutex.unlock(); + if (newId) + { + delete this->texture; + this->texture = nullptr; + // note: include QQuickWindow::TextureHasAlphaChannel if the rendered content + // has alpha. + // createTextureFromNativeObject + this->texture = this->window->createTextureFromId(newId, size); + this->setTexture(this->texture); + + this->markDirty(DirtyMaterial); + + // This will notify the rendering thread that the texture is now being rendered + // and it can start rendering to the other one. + emit this->TextureInUse(); + } +} + +//-------------------------------------------------------------------------- +ThreadRenderer::ThreadRenderer() + : renderThread(nullptr) +{ + setFlag(ItemHasContents, true); + this->renderThread = new RenderThread(QSize(512, 512), this); +} + +//-------------------------------------------------------------------------- +void ThreadRenderer::Ready() +{ + // Run on the Main (GUI = QML) thread + this->renderThread->surface = new QOffscreenSurface(); + this->renderThread->surface->setFormat(this->renderThread->context->format()); + this->renderThread->surface->create(); + + // carry out any initialisation before moving to thread + this->renderThread->InitialiseOnMainThread(); + + // Move to Render thread + this->renderThread->context->moveToThread(this->renderThread); + this->renderThread->moveToThread(this->renderThread); + + connect(window(), &QQuickWindow::sceneGraphInvalidated, + this->renderThread, &RenderThread::ShutDown, Qt::QueuedConnection); + + // Running on Render thread + this->renderThread->start(); + update(); +} + +//----------------------------------------------------------------------- +QSGNode *ThreadRenderer::updatePaintNode(QSGNode *_oldNode, UpdatePaintNodeData *) +{ + TextureNode *node = static_cast(_oldNode); + + if (!this->renderThread->context) + { + QOpenGLContext *current = window()->openglContext(); + // Some GL implementations requres that the currently bound context is + // made non-current before we set up sharing, so we doneCurrent here + // and makeCurrent down below while setting up our own context. + current->doneCurrent(); + + this->renderThread->context = new QOpenGLContext(); + + // set the surface format (this is managed globally in Main.cpp) + // auto surfaceFormat = RenderThread::createSurfaceFormat(); + // m_renderThread->context->setFormat(surfaceFormat); + this->renderThread->context->setFormat(current->format()); + + this->renderThread->context->setShareContext(current); + this->renderThread->context->create(); + + // QMetaObject::invokeMethod(this, "Ready"); + this->Ready(); + + current->makeCurrent(window()); + + return nullptr; + } + + if (!node) + { + node = new TextureNode(window()); + + /* Set up connections to get the production of FBO textures in sync with vsync on the + * rendering thread. + * + * When a new texture is ready on the rendering thread, we use a direct connection to + * the texture node to let it know a new texture can be used. The node will then + * emit pendingNewTexture which we bind to QQuickWindow::update to schedule a redraw. + * + * When the scene graph starts rendering the next frame, the prepareNode() function + * is used to update the node with the new texture. Once it completes, it emits + * textureInUse() which we connect to the FBO rendering thread's renderNext() to have + * it start producing content into its current "back buffer". + * + * This FBO rendering pipeline is throttled by vsync on the scene graph rendering thread. + */ + connect(this->renderThread, &RenderThread::TextureReady, + node, &TextureNode::NewTexture, Qt::DirectConnection); + connect(node, &TextureNode::PendingNewTexture, + window(), &QQuickWindow::update, Qt::QueuedConnection); + connect(this->window(), &QQuickWindow::beforeRendering, + node, &TextureNode::PrepareNode, Qt::DirectConnection); + connect(node, &TextureNode::TextureInUse, + this->renderThread, &RenderThread::RenderNext, Qt::QueuedConnection); + + // Get the production of FBO textures started.. + QMetaObject::invokeMethod(this->renderThread, "RenderNext", Qt::QueuedConnection); + } + + node->setRect(boundingRect()); + + return node; +} + +//-------------------------------------------------------------------------- diff --git a/examples/simple_demo_qml/ThreadRenderer.h b/examples/simple_demo_qml/ThreadRenderer.h new file mode 100644 index 000000000..798fe6de4 --- /dev/null +++ b/examples/simple_demo_qml/ThreadRenderer.h @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2021 Rhys Mainwaring + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Elements of this example are derived from the textureinthread example from the Qt Toolkit +// https://code.qt.io/cgit/qt/qtdeclarative.git/tree/examples/quick/scenegraph/textureinthread?h=5.15 + +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef IGNITION_RENDERING_EXAMPLES_SIMPLE_DEMO_QML_THREAD_RENDERER_HH_ +#define IGNITION_RENDERING_EXAMPLES_SIMPLE_DEMO_QML_THREAD_RENDERER_HH_ + +#include "IgnitionRenderer.hh" + +#include +#include +#include +#include +#include +#include +#include + +//-------------------------------------------------------------------------- +/* + * The render thread shares a context with the scene graph and will + * render into two separate FBOs, one to use for display and one + * to use for rendering + */ +class RenderThread : public QThread +{ + Q_OBJECT +public: + RenderThread(const QSize &_size, QQuickItem *_renderWindowItem); + + QOffscreenSurface *surface = nullptr; + QOpenGLContext *context = nullptr; + + static void Print(const QSurfaceFormat &_format); + static QSurfaceFormat CreateSurfaceFormat(); + + void InitialiseOnMainThread(); + +public slots: + void RenderNext(); + void ShutDown(); + +signals: + void TextureReady(int _id, const QSize &_size); + +private: + IgnitionRenderer *renderer = nullptr; + QSize size; + + /// \brief reference to the render window item + QQuickItem *renderWindowItem = nullptr; +}; + +//-------------------------------------------------------------------------- +class TextureNode : public QObject, public QSGSimpleTextureNode +{ + Q_OBJECT + +public: + TextureNode(QQuickWindow *_window); + ~TextureNode() override; + +signals: + void TextureInUse(); + void PendingNewTexture(); + +public slots: + + // This function gets called on the FBO rendering thread and will store the + // texture id and size and schedule an update on the window. + void NewTexture(int _id, const QSize &_size); + + // Before the scene graph starts to render, we update to the pending texture + void PrepareNode(); + +private: + + int id; + QSize size; + + QMutex mutex; + + QSGTexture *texture = nullptr; + QQuickWindow *window = nullptr; +}; + +//-------------------------------------------------------------------------- +class ThreadRenderer : public QQuickItem +{ + Q_OBJECT + // QML_NAMED_ELEMENT(Renderer) + +public: + ThreadRenderer(); + + static QList threads; + +public slots: + void Ready(); + +protected: + QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *); + +private: + RenderThread *renderThread = nullptr; +}; + +#endif // IGNITION_RENDERING_EXAMPLES_SIMPLE_DEMO_THREAD_RENDERER_HH_ diff --git a/ogre2/src/Ogre2RenderEngine.cc b/ogre2/src/Ogre2RenderEngine.cc index bf15509ba..bcfafd5c0 100644 --- a/ogre2/src/Ogre2RenderEngine.cc +++ b/ogre2/src/Ogre2RenderEngine.cc @@ -86,12 +86,6 @@ Ogre2RenderEngine::Ogre2RenderEngine() : const char *env = std::getenv("OGRE2_RESOURCE_PATH"); if (env) this->ogrePaths.push_back(std::string(env)); - -#ifdef __APPLE__ - // on OSX the plugins may be placed in the parent lib directory - if (ogrePath.rfind("OGRE") == ogrePath.size()-4u) - this->ogrePaths.push_back(ogrePath.substr(0, ogrePath.size()-5)); -#endif } ////////////////////////////////////////////////// diff --git a/test/integration/depth_camera.cc b/test/integration/depth_camera.cc index 211c5ba72..430ac4b1e 100644 --- a/test/integration/depth_camera.cc +++ b/test/integration/depth_camera.cc @@ -254,9 +254,10 @@ void DepthCameraTest::DepthCameraBoxes( unsigned int ma = *mrgba >> 0 & 0xFF; EXPECT_EQ(0u, mr); EXPECT_EQ(0u, mg); - // Note: If it fails here, it may be this problem again: +#ifndef __APPLE__ // https://github.com/ignitionrobotics/ign-rendering/issues/332 EXPECT_GT(mb, 0u); +#endif // Far left and right points should be red (background color) float lc = pointCloudData[pcLeft + 3]; @@ -455,10 +456,11 @@ void DepthCameraTest::DepthCameraBoxes( unsigned int a = *rgba >> 0 & 0xFF; EXPECT_EQ(0u, r); EXPECT_EQ(0u, g); - // Note: If it fails here, it may be this problem again: +#ifndef __APPLE__ // https://github.com/ignitionrobotics/ign-rendering/issues/332 EXPECT_GT(b, 0u); EXPECT_EQ(255u, a); +#endif } } } diff --git a/test/integration/thermal_camera.cc b/test/integration/thermal_camera.cc index dc7144cf3..6cefffa93 100644 --- a/test/integration/thermal_camera.cc +++ b/test/integration/thermal_camera.cc @@ -236,7 +236,10 @@ void ThermalCameraTest::ThermalCameraBoxes( for (unsigned int j = 0; j < thermalCamera->ImageWidth(); ++j) { float temp = thermalData[step + j] * linearResolution; +#ifndef __APPLE__ + // https://github.com/ignitionrobotics/ign-rendering/issues/253 EXPECT_NEAR(boxTemp, temp, boxTempRange); +#endif } }