diff --git a/CMakeLists.txt b/CMakeLists.txt index d80b1bddacb..2f544ce7bf3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ option(RADIUM_GENERATE_LIB_ENGINE "Include Radium::Engine in CMake project." ON) option(RADIUM_GENERATE_LIB_GUI "Include Radium::Gui in CMake project." ON) option(RADIUM_GENERATE_LIB_PLUGINBASE "Include Radium::PluginBase in CMake project." ON) option(RADIUM_GENERATE_LIB_HEADLESS "Include Radium::Headless in CMake project." ON) +option(RADIUM_GENERATE_LIB_DATAFLOW "Include Radium::Dataflow* in CMake project." ON) option( RADIUM_UPDATE_VERSION "Update version file each time the project is compiled (update compilation time in version.cpp)." @@ -290,6 +291,7 @@ message_setting("RADIUM_GENERATE_LIB_CORE") message_setting("RADIUM_GENERATE_LIB_ENGINE") message_setting("RADIUM_GENERATE_LIB_GUI") message_setting("RADIUM_GENERATE_LIB_HEADLESS") +message_setting("RADIUM_GENERATE_LIB_DATAFLOW") message_setting("RADIUM_GENERATE_LIB_IO") message_setting("RADIUM_GENERATE_LIB_PLUGINBASE") string(REPLACE ";" " " COMPONENTS_LIST "${RADIUM_COMPONENTS}") diff --git a/cmake/RadiumSetupFunctions.cmake b/cmake/RadiumSetupFunctions.cmake index 9c4c861422e..39e807b228d 100644 --- a/cmake/RadiumSetupFunctions.cmake +++ b/cmake/RadiumSetupFunctions.cmake @@ -966,18 +966,27 @@ function(configure_radium_library) # add the cmake command to install target add_custom_install_target(${ARGS_TARGET}) + get_target_property(TargetType ${ARGS_TARGET} TYPE) + if(TargetType STREQUAL INTERFACE_LIBRARY) + set(PropertyQualifier INTERFACE) + else() + set(PropertyQualifier PUBLIC) + target_compile_definitions(${ARGS_TARGET} PRIVATE ${ARGS_TARGET}_EXPORTS) + endif() + if(CMAKE_BUILD_TYPE STREQUAL "Debug") - target_compile_definitions(${ARGS_TARGET} PUBLIC _DEBUG) - if(MSVC OR MSVC_IDE) + target_compile_definitions(${ARGS_TARGET} ${PropertyQualifier} _DEBUG) + if((MSVC OR MSVC_IDE) AND NOT (TargetType STREQUAL INTERFACE_LIBRARY)) install(FILES $ DESTINATION bin) endif() endif() - target_compile_features(${ARGS_TARGET} PUBLIC cxx_std_17) + + target_compile_features(${ARGS_TARGET} ${PropertyQualifier} cxx_std_17) if(OPENMP_FOUND) - target_link_libraries(${ARGS_TARGET} PUBLIC OpenMP::OpenMP_CXX) + target_link_libraries(${ARGS_TARGET} ${PropertyQualifier} OpenMP::OpenMP_CXX) endif(OPENMP_FOUND) - target_compile_definitions(${ARGS_TARGET} PRIVATE ${ARGS_TARGET}_EXPORTS) - target_include_directories(${ARGS_TARGET} PUBLIC $) + target_include_directories(${ARGS_TARGET} ${PropertyQualifier} $) + add_library(${ARGS_NAMESPACE}::${ARGS_TARGET} ALIAS ${ARGS_TARGET}) install( diff --git a/doc/basics/dependencies.md b/doc/basics/dependencies.md index ab706eebad4..ee134f70a7c 100644 --- a/doc/basics/dependencies.md +++ b/doc/basics/dependencies.md @@ -7,6 +7,7 @@ Radium relies on several external libraries to load files or to represent some d * [Engine] glm, globjects, glbindings, tinyEXR * [IO] Assimp * [Gui] Qt Core, Qt Widgets and Qt OpenGL v5.5+ (5.14 at least, Qt6 support is experimental), PowerSlider +* [Dataflow] RadiumNodeEditor * [doc] Doxygen-awesome-css * stb_image @@ -100,6 +101,7 @@ set(tinyEXR_DIR "/path/to/external/install/share/tinyEXR/cmake/" CACHE PATH "My set(assimp_DIR "/path/to/external/install/lib/cmake/assimp-5.0/" CACHE PATH "My assimp location") set(tinyply_DIR "/path/to/external/install/lib/cmake/tinyply/" CACHE PATH "My tinyply location") set(PowerSlider_DIR "/path/to/external/install/lib/cmake/PowerSlider/" CACHE PATH "My PowerSlider location") +set(RadiumNodeEditor_DIR "/path/to/external/install/lib/cmake/RadiumNodeEditor/" CACHE PATH "My NodeEditor") set(RADIUM_IO_ASSIMP ON CACHE BOOL "Radium uses assimp io") set(RADIUM_IO_TINYPLY ON CACHE BOOL "Radium uses tinyply io") ~~~ @@ -122,6 +124,7 @@ To this end, just provide the corresponding '*_DIR' to cmake at configuration ti Currently supported (note that these paths must refer to the installation directory of the corresponding library): +* `RadiumNodeEditor_DIR` * `assimp_DIR` * `tinyply_DIR` * `PowerSlider_DIR` @@ -137,6 +140,8 @@ Currently supported (note that these paths must refer to the installation direct Radium is compiled and tested with specific version of dependencies, as given in the external's folder CMakeLists.txt and state here for the record +* RadiumNodeEditor: https://github.com/MathiasPaulin/RadiumQtNodeEditor.git, [main], + * with options `None` * assimp: https://github.com/assimp/assimp.git, [tags/v5.0.1], * with options `-DASSIMP_BUILD_ASSIMP_TOOLS=False -DASSIMP_BUILD_SAMPLES=False -DASSIMP_BUILD_TESTS=False -DIGNORE_GIT_HASH=True -DASSIMP_NO_EXPORT=True` * tinyply: https://github.com/ddiakopoulos/tinyply.git, [tags/2.3.2], diff --git a/doc/concepts.md b/doc/concepts.md index 91f1f94b593..1dcfefea3b1 100644 --- a/doc/concepts.md +++ b/doc/concepts.md @@ -5,3 +5,4 @@ * \subpage eventSystem * \subpage pluginSystem * \subpage forwardRenderer +* \subpage nodeSystem diff --git a/doc/concepts/dataflow/HelloGraph.json b/doc/concepts/dataflow/HelloGraph.json new file mode 100644 index 00000000000..ec1e743560f --- /dev/null +++ b/doc/concepts/dataflow/HelloGraph.json @@ -0,0 +1,71 @@ +{ + "instance": "helloGraph", + "model": { + "graph": { + "connections": [ + { + "in_node": "Filter", + "in_port": "in", + "out_node": "Source", + "out_port": "to" + }, + { + "in_node": "Filter", + "in_port": "f", + "out_node": "Selector", + "out_port": "f" + }, + { + "in_node": "Sink", + "in_port": "from", + "out_node": "Filter", + "out_port": "out" + } + ], + "factories": [], + "nodes": [ + { + "instance": "Source", + "model": { + "name": "Source>" + }, + "position": { + "x": -195.0, + "y": -82.0 + } + }, + { + "instance": "Selector", + "model": { + "name": "Source>" + }, + "position": { + "x": -279.0, + "y": 35.0 + } + }, + { + "instance": "Filter", + "model": { + "name": "Filter>" + }, + "position": { + "x": 44.0, + "y": -45.0 + } + }, + { + "instance": "Sink", + "model": { + "name": "Sink>" + }, + "position": { + "x": 267.0, + "y": 12.0 + } + } + ] + }, + "name": "Core DataflowGraph" + } +} diff --git a/doc/concepts/nodesystem.md b/doc/concepts/nodesystem.md new file mode 100644 index 00000000000..e484a849cf0 --- /dev/null +++ b/doc/concepts/nodesystem.md @@ -0,0 +1,199 @@ +\page nodeSystem Radium node system +[TOC] + +# Radium node system + +Radium-Engine embed a node system allowing to develop computation graph using an adaptation of dataflow programming. +This documentation explain the concepts used in the node system and how to develop computation graph using the Core +node system and how to extend the Core node system to be used in specific Radium-Engine application or library. + +## Structure and usage of the Radium::Dataflow component + +When building the Radium-Engine libraries, the node system is available from the Radium::Dataflow component. +The availability of this component in the set of built/installed libraries is managed using the +`RADIUM_GENERATE_LIB_DATAFLOW` cmake option (set to `ON` by default) of the main Radium-Engine CMakeLists.txt. + +The Radium::Dataflow component is a header-only library with is linked against three sub-components : + +- **DataflowCore** library defining the Core node system and a set Core Nodes allowing to develop several computation + graph. +- **DataflowQtGui** library defining Qt based Gui elements to edit and interact with a computation graph. +- **DataflowRendering** library defining specific nodes to be used by a Graph-based renderer allowing to easily define + custom renderers. + +When defining the CMakeLists.txt configuration file for an application/library that will build upon the node system, +The RadiumDataflow component might be requested in several way : + +- `find_package(Radium REQUIRED COMPONENTS Dataflow)`. This will define the imported target `Radium::Dataflow` that +gives access to all the available sub-components at once but also through imported targets `Radium::DataflowCore`, +`Radium::DataflowQtGui` and `Radium::DataflowRendering`. +- `find_package(Radium REQUIRED COMPONENTS DataflowCore)`. This will define the imported target `Radium::DataflowCore` +only. +- `find_package(Radium REQUIRED COMPONENTS DataflowQtGui)`. This will define the imported target `Radium::DataflowQtGui` +only, with transitive dependencies on `Radium::DataflowCore`. +- `find_package(Radium REQUIRED COMPONENTS DataflowRendering)`. This will define the imported target +`Radium::DataflowRendering` only, with transitive dependencies on `Radium::DataflowCore`. + +The targets that depends on a Dataflow components should then be configured as any target and linked against the +requested dataflow component, by, e.g, adding the line +`target_link_libraries(target_name PUBLIC Radium::Dataflow)` (or the only needed subcomponent). + +## Concepts of the node system + +The node system allow to build computation graph that takes its input from some _data source_ and store their results +in some _data sink_ after applying several _functions_ on the data. + +Computation graphs can be serialized and un-serialized in json format. The serialization process is nevertheless limited +to serializable data stored on the node and it is of the responsibility of the application to manage non serializable +data such as, e.g. anonymous functions (lambdas, functors, ...) dynamically defined by the application. + +The Radium node system relies on the following concepts + +- _Node_ (see Ra::Dataflow::Core::Node for reference manual) : a node represent a function that will be executed on +several strongly typed input data, defining the definition domain of the function, to produce some strongly typed +output data, defining the definition co-domain of the function. + + The input and output data are accessed through _ports_ allowing to connect nodes together to form a graph. + + The node profile is implicitly defined by its domain and co-domain. + + A node can be specialized to be a _data source_ node (empty domain) or a _data sink_ node (empty co-domain). + These specific nodes define the input and output of a complex _computation graph_ + +- _Port_ (see Ra::Dataflow::Core::PortBase for reference manual) : a port represent an element of the node +profile and allow to build the computation graph by linking ports together, implicitly defining _links_. + + A port gives access to a strongly typed data and, while implementing the general Ra::Dataflow::Core::PortBase +interface should be specialized to be either an input port (element of the definition domain of a node) through the +instancing of the template Ra::Dataflow::Core::PortIn or to an output port (element of the definition +co-domain of a node) through the instancing of the template Ra::Dataflow::Core::PortOut. + + When a node executes its function, it takes its parameter from its input ports and set the result on the output port. + + An output port can be connected to an input port of the same _DataType_ to build the computation graph. +- _Graph_ (see Ra::Dataflow::Core::DataflowGraph for reference manual) : a graph is a set of node connected through +their ports so that they define a direct acyclic graph (DAG) representing a complex function. The DAG represents +connections from some _data source_ nodes to some _data sink_ nodes through + + Once built by adding nodes and links, a graph should be _compiled_ so that the system verify its validity +(DAG, types, connections, ...). + + Once compiled, a graph can be _executed_. + The input data of the computation graph should be set on the available _data sources_ of the graph and the results +fetched from the _data sinks_. + + As a graph can be used as a node (a sub graph) in any other graph. When doing this, all _data sources_ and +_data sinks_ nodes are associated with _interface ports_ and these interface ports are added as _input_ or _output_ +ports on the graph so that links can be defined using these ports. + + _input_ and _output_ ports of a graph can also be accessed directly from the application using _data setters_ and +_data getters_ fetched from the graph. These _data setters_ and _data getters_ allows to use any graph without the +need to know explicitly their _data sources_ and _data sinks_ nor defining ports to be linked with the _input_ and +_output_ ports of the graph. + +- _Factories_ (see Ra::Dataflow::Core::NodeFactoriesManager for reference manual) : the serialization of a graph output +a set of json object describing the graph. If serialization is always possible, care must be taken for the system to +manage un-serilization of any nodes. + + When serializing a graph, the json representing a node contains the type (the name of the concrete C++ class) of the +node and several other properties of the node system. When un-serializing a graph, nodes will be automatically +instanced from their type. +The instantiation of a node is made using services offered by node factories and associated to the node type. So, in +order to be un-serializable, each node must register its type to a factory and each graph must refer to the factories used +to instantiate its node. + +## HelloGraph : your first program that uses the node system + +The example application examples/HelloGraph shows how to define a computation graph to apply filtering on a collection. +In this example, whose code is detailed below, the following graph is built and executed using different input data. + +![HelloGraph computation graph](images/HelloGraph.png) + +This graphs has two inputs, corresponding to the two **Source< ... >** nodes. These input will deliver to the +computation graph : + +- from a Ra::Dataflow::Core::Sources::SingleDataSourceNode, a vector of scalars, whose container type is +Ra::Core::VectorArray (abridged here as a RaVector) and value type is _Scalar_ (float in the default Radium-Engine +configuration), +- from a Ra::Dataflow::Core::Sources::FunctionSourceNode a predicate whose type is _std::function_ +which returns _true_ if its parameter is valid according to some decision process. + +These two _sources_ are linked to the input of a Ra::Dataflow::Core::Functionals::FilterNode, here represented by the +**Filter< ... >** node. This node select from its **in** input only the values validated by the predicate **f** and +built its output **out** with these values. + +The result of this filtering is linked to the graph output, corresponding to the Ra::Dataflow::Core::Sinks::SinkNode +**Sink< ... >**. + +Once the graph is built and compile, the HelloGraph application sent different input to the graph and print the result +of the computation. + +To develop such an application, the following should be done + +### 1. Building and inspecting the graph + +First, an object of type Ra::Dataflow::Core::DataflowGraph is instanced : +\snippet examples/DataflowExamples/HelloGraph/main.cpp Creating an empty graph + +Then, the nodes are instanced +\snippet examples/DataflowExamples/HelloGraph/main.cpp Creating Nodes + +and added to the graph +\snippet examples/DataflowExamples/HelloGraph/main.cpp Adding Nodes to the graph + +Links between ports are added to the graph, and if an error is detected, due to e.g. port type incompatiblitiy, it is +reported +\snippet examples/DataflowExamples/HelloGraph/main.cpp Creating links between Nodes + +Once the graph is built, it can be inspected using dedicated methods. +\snippet examples/DataflowExamples/HelloGraph/main.cpp Inspect the graph interface : inputs and outputs port + +For the graph constructed above, this prints on stdout + +```text +Input ports (2) are : + "Selector_f" accepting type function + "Source_to" accepting type RaVector +Output ports (1) are : + "Sink_from" generating type RaVector +``` + +### 2. Compiling the graph and getting input/output accessors + +In order to use the graph as a function acting on its input, it should be first compiled by +\snippet examples/DataflowExamples/HelloGraph/main.cpp Verifying the graph can be compiled + +If the compilation success the accessors for the input data and the output result might be fetched from the graph +\snippet examples/DataflowExamples/HelloGraph/main.cpp Configure the interface ports (input and output of the graph) + +Here, the accessor `input` allows to set the pointer on the `RaVector` to be processed while the accessor `selector` +allows to set the predicate to evaluate when filtering the collection. This predicates select values les than `0.5` + +The accessor `output` will allow, once the graph is executed, to get a reference to or to copy the resulting values. + +### 3. Executing the graph + +Once the input data are available (in this example, the `test` vector is filled with 10 random values between 0 and 1), +the graph can be executed and a reference to the resulting vector can be fetched using +\snippet examples/DataflowExamples/HelloGraph/main.cpp Execute the graph + +### 4. Multiple run of the graph on different input + +As accessors use pointers and references on the data sent to / fetched from the graph, the HelloGraph example shows how +to change the input using different available means so that every run of the graph process different values. +See the file examples/DataflowExamples/HelloGraph/main.cpp for all the details. + +## Examples of graphs and of programming custom nodes + +The unittests developed alongside the Radium::Dataflow component, and located in the directory +`tests/unittest/Dataflow/` of the Radium-Engine source tree, can be used to learn the following : + +- sourcesandsinks.cpp : demonstrate the default supported types for sources and sinks node. +- nodes.cpp : demonstrate the development of a more complex graph implementing transform/reduce +on several collections using different reduction operators. +- graphinspect.cpp : demonstrate the way a graph can be inspected to discover its structure. +- serialization.cpp : demonstrate how to save/load a graph from a json file and use it in an +application. +- customnodes.cpp : demonstrate how it is simple to develop your own node type (in C++) and +use your nodes alongside standard nodes. + \snippet unittest/Dataflow/customnodes.cpp Develop a custom node diff --git a/doc/images/HelloGraph.png b/doc/images/HelloGraph.png new file mode 100644 index 00000000000..f7dfe06218d Binary files /dev/null and b/doc/images/HelloGraph.png differ diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c17881c905b..8d69d7d9011 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -15,6 +15,7 @@ foreach( APP CoreExample CustomCameraManipulator + DataflowExamples DrawPrimitives EntityAnimation EnvMap diff --git a/examples/DataflowExamples/CMakeLists.txt b/examples/DataflowExamples/CMakeLists.txt new file mode 100644 index 00000000000..c3ebc4be8b8 --- /dev/null +++ b/examples/DataflowExamples/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0042 NEW) + +project(DataflowExamples VERSION 1.0.0) + +add_custom_target(${PROJECT_NAME}) +add_custom_target(Install_${PROJECT_NAME}) +foreach(APP GraphSerialization HelloGraph GraphEditor FunctionalsGraph GraphRendering) + add_subdirectory(${APP}) + add_dependencies(${PROJECT_NAME} ${APP}) + add_dependencies(Install_${PROJECT_NAME} Install_${APP}) +endforeach() diff --git a/examples/DataflowExamples/FunctionalsGraph/CMakeLists.txt b/examples/DataflowExamples/FunctionalsGraph/CMakeLists.txt new file mode 100644 index 00000000000..3cf726e8a27 --- /dev/null +++ b/examples/DataflowExamples/FunctionalsGraph/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0071 NEW) + +if(APPLE) + cmake_policy(SET CMP0042 NEW) +endif(APPLE) + +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +project(FunctionalsGraph VERSION 0.0.1) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Set default install location to installed- folder in build dir we do not want to +# install to /usr by default +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX + "${CMAKE_CURRENT_BINARY_DIR}/installed-${CMAKE_CXX_COMPILER_ID}-${CMAKE_BUILD_TYPE}" + CACHE PATH "Install path prefix, prepended onto install directories." FORCE + ) + message(STATUS "Set install prefix to ${CMAKE_INSTALL_PREFIX}") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# /////////////////////////////// +find_package(Radium REQUIRED COMPONENTS Dataflow) + +# -------------------------------------------------------------------------------------------------- + +set(app_sources main.cpp) +# set(app_headers) + +# delete ${app_headers} +add_executable(${PROJECT_NAME} ${app_sources}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) + +target_link_libraries(${PROJECT_NAME} PUBLIC Radium::Dataflow) + +configure_radium_app(NAME ${PROJECT_NAME}) diff --git a/examples/DataflowExamples/FunctionalsGraph/main.cpp b/examples/DataflowExamples/FunctionalsGraph/main.cpp new file mode 100644 index 00000000000..9d26bd23f0e --- /dev/null +++ b/examples/DataflowExamples/FunctionalsGraph/main.cpp @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include +#include + +#include + +using namespace Ra::Dataflow::Core; + +/* ----------------------------------------------------------------------------------- */ +/** + * \brief Demonstrate how to use a graph to filter a collection + */ +int main( int argc, char* argv[] ) { + //! [Creating an empty graph] + DataflowGraph g { "helloGraph" }; + //! [Creating an empty graph] + + //! [Creating Nodes] + using TransformOperatorSource = Sources::FunctionSourceNode; + auto sourceNode = new Sources::SingleDataSourceNode>( "Source" ); + auto mapSource = new TransformOperatorSource( "MapOperator" ); + auto transformNode = new Functionals::TransformNode>( "Transformer" ); + // can't use auto here, must explicitely define the type + Functionals::TransformNode>::TransformOperator oneMinusMe = + []( const Scalar& i ) -> Scalar { return 1_ra - i; }; + transformNode->setOperator( oneMinusMe ); + + auto reduceNode = new Functionals::ReduceNode>( "Minimum" ); + Functionals::ReduceNode>::ReduceOperator getMin = + []( const Scalar& a, const Scalar& b ) -> Scalar { return std::min( a, b ); }; + reduceNode->setOperator( getMin, std::numeric_limits::max() ); + + auto scalarSinkNode = new Sinks::SinkNode( "ScalarSink" ); + + auto sinkNode = new Sinks::SinkNode>( "Sink" ); + //! [Creating Nodes] + + //! [Adding Nodes to the graph] + g.addNode( std::unique_ptr( sourceNode ) ); + g.addNode( std::unique_ptr( mapSource ) ); + g.addNode( std::unique_ptr( transformNode ) ); + g.addNode( std::unique_ptr( reduceNode ) ); + g.addNode( std::unique_ptr( sinkNode ) ); + g.addNode( std::unique_ptr( scalarSinkNode ) ); + //! [Adding Nodes to the graph] + + //! [Creating links between Nodes] + g.addLink( sourceNode, "to", transformNode, "in" ); + g.addLink( transformNode, "out", sinkNode, "from" ); + g.addLink( transformNode, "out", reduceNode, "in" ); + g.addLink( reduceNode, "out", scalarSinkNode, "from" ); + //! [Creating links between Nodes] + + //! [Verifing the graph can be compiled] + if ( !g.compile() ) { + std::cout << " compilation failed"; + return 1; + } + //! [Verifing the graph can be compiled] + + ///! [Configure the interface ports (input and output of the graph) + auto input = g.getDataSetter( "Source_to" ); + std::vector test; + input->setData( &test ); + + auto opSetter = g.getDataSetter( "MapOperator_f" ); + // can't use auto here, must explicitely define the type + TransformOperatorSource::function_type doubleMe = []( const Scalar& i ) -> Scalar { + return 2_ra * i; + }; + opSetter->setData( &doubleMe ); + + auto output = g.getDataGetter( "Sink_from" ); + auto minimum = g.getDataGetter( "ScalarSink_from" ); + // The reference to the result will not be available before the first run + // auto& result = output->getData>(); + ///! [Configure the interface ports (input and output of the graph) + + //! [Initializing input variable to test the graph] + test.reserve( 10 ); + std::mt19937 gen( 0 ); + std::uniform_real_distribution<> dis( 0.0, 1.0 ); + // Fill the vector with random numbers between 0 and 1 + for ( int n = 0; n < test.capacity(); ++n ) { + test.push_back( dis( gen ) ); + } + + std::cout << "Input values : \n\t"; + for ( auto ord : test ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Initializing input variable to test the graph] + + //! [Execute the graph] + g.execute(); + // The reference to the result is now available + auto& result = output->getData>(); + auto& minValue = minimum->getData(); + //! [Execute the graph] + + //! [Print the output result] + std::cout << "Output values (oneMinusMe): " << result.size() << "\n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + std::cout << "Min value is : " << minValue << "\n"; + //! [Print the output result] + + //! [Verify the result] + std::cout << "Verifying the result : \n\t"; + + bool execOK { true }; + for ( int i = 0; i < 10; ++i ) { + if ( result[i] != oneMinusMe( test[i] ) ) { + std::cout << "Error at index " << i << " : " << result[i] << " should be " + << oneMinusMe( test[i] ) << "\n\t"; + execOK = false; + } + } + if ( execOK ) { std::cout << "OK\n"; } + else { std::cout << "NOK\n"; } + //! [Verify the result] + + //! [Modifying the graph to add link to map operator] + if ( !g.addLink( mapSource, "f", transformNode, "f" ) ) { + std::cout << "Unable to link port f (" + << ") from " << mapSource->getInstanceName() << " to port f(" + << ") of " << transformNode->getInstanceName() << "\n"; + return 1; + } + //! [Modifying the graph to add link to map operator] + + //! [Execute and test the result] + std::cout << "Rerun the graph with different operator : \n"; + g.execute(); + std::cout << "Output values (doubleMe): " << result.size() << "\n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + std::cout << "Min value is : " << minValue << "\n"; + execOK = true; + for ( int i = 0; i < 10; ++i ) { + if ( result[i] != doubleMe( test[i] ) ) { + std::cout << "Error at index " << i << " : " << result[i] << " should be " + << doubleMe( test[i] ) << "\n\t"; + execOK = false; + } + } + if ( execOK ) { std::cout << "OK\n"; } + else { std::cout << "NOK\n"; } + //! [Execute and test the result] + + return 0; +} diff --git a/examples/DataflowExamples/GraphEditor/CMakeLists.txt b/examples/DataflowExamples/GraphEditor/CMakeLists.txt new file mode 100644 index 00000000000..fdfdb2dcdf2 --- /dev/null +++ b/examples/DataflowExamples/GraphEditor/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0071 NEW) + +if(APPLE) + cmake_policy(SET CMP0042 NEW) +endif(APPLE) + +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +project(GraphEditor VERSION 0.0.1) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Set default install location to installed- folder in build dir we do not want to +# install to /usr by default +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX + "${CMAKE_CURRENT_BINARY_DIR}/installed-${CMAKE_CXX_COMPILER_ID}-${CMAKE_BUILD_TYPE}" + CACHE PATH "Install path prefix, prepended onto install directories." FORCE + ) + message(STATUS "Set install prefix to ${CMAKE_INSTALL_PREFIX}") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# /////////////////////////////// +find_package(Radium REQUIRED COMPONENTS Dataflow) + +# Qt utility functions +include(QtFunctions) + +# Find Qt packages +find_qt_package(COMPONENTS Core Widgets REQUIRED) +set(QT_DEFAULT_MAJOR_VERSION ${QT_DEFAULT_MAJOR_VERSION} PARENT_SCOPE) +set(Qt_LIBRARIES Qt::Core Qt::Widgets) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +qt_wrap_ui(gui_uis ${gui_uis}) + +# -------------------------------------------------------------------------------------------------- +set(app_sources main.cpp) + +# set(app_headers) + +add_executable(${PROJECT_NAME} ${app_sources}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) + +target_link_libraries(${PROJECT_NAME} PUBLIC ${Qt_LIBRARIES} Radium::Dataflow) + +configure_radium_app(NAME ${PROJECT_NAME}) diff --git a/examples/DataflowExamples/GraphEditor/main.cpp b/examples/DataflowExamples/GraphEditor/main.cpp new file mode 100644 index 00000000000..3a9a31c6583 --- /dev/null +++ b/examples/DataflowExamples/GraphEditor/main.cpp @@ -0,0 +1,24 @@ +#include +#include +#include + +#include + +int main( int argc, char* argv[] ) { + QApplication app( argc, argv ); + QCoreApplication::setOrganizationName( "STORM-IRIT" ); + QCoreApplication::setApplicationName( "Radium NodeGraph Editor" ); + QCoreApplication::setApplicationVersion( QT_VERSION_STR ); + QCommandLineParser parser; + parser.setApplicationDescription( QCoreApplication::applicationName() ); + parser.addHelpOption(); + parser.addVersionOption(); + parser.addPositionalArgument( "file", "The file to open." ); + parser.process( app ); + + Ra::Dataflow::QtGui::GraphEditor::GraphEditorWindow mainWin; + if ( !parser.positionalArguments().isEmpty() ) + mainWin.loadFile( parser.positionalArguments().first() ); + mainWin.show(); + return app.exec(); +} diff --git a/examples/DataflowExamples/GraphRendering/CMakeLists.txt b/examples/DataflowExamples/GraphRendering/CMakeLists.txt new file mode 100644 index 00000000000..7e461459903 --- /dev/null +++ b/examples/DataflowExamples/GraphRendering/CMakeLists.txt @@ -0,0 +1,72 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0071 NEW) + +if(APPLE) + cmake_policy(SET CMP0042 NEW) +endif(APPLE) + +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +project(GraphRendering VERSION 0.0.1) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Set default install location to installed- folder in build dir we do not want to +# install to /usr by default +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX + "${CMAKE_CURRENT_BINARY_DIR}/installed-${CMAKE_CXX_COMPILER_ID}-${CMAKE_BUILD_TYPE}" + CACHE PATH "Install path prefix, prepended onto install directories." FORCE + ) + message(STATUS "Set install prefix to ${CMAKE_INSTALL_PREFIX}") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# /////////////////////////////// +find_package(Radium REQUIRED COMPONENTS Dataflow) + +# Qt utility functions +include(QtFunctions) + +# Find Qt packages +find_qt_package(COMPONENTS Core Widgets REQUIRED) +set(Qt_LIBRARIES Qt::Core Qt::Widgets) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +# qt_wrap_ui(gui_uis ${gui_uis}) + +# -------------------------------------------------------------------------------------------------- + +set(app_sources main.cpp) +# set(app_headers) + +# delete ${app_headers} +add_executable(${PROJECT_NAME} ${app_sources}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) + +target_link_libraries(${PROJECT_NAME} PUBLIC ${Qt_LIBRARIES} Radium::Dataflow) + +# ------------------------------------------------------------------------------ +# RadiumGlTF is available here https://gitlab.irit.fr/storm/repos/radium/libgltf.git (using the +# right branch). Compile RadiumGlTF, then configure using cmake [your configure args] +# -DRadiumGlTF_DIR="path/to/RadiumGlTF/build-dir/src/RadiumGlTF" -DUSE_RADIUMGLTF=ON to use it for +# this example. +option(USE_RADIUMGLTF "Enable loading/saving files with RadiumGltf extension" OFF) +if(USE_RADIUMGLTF) + message(STATUS "${PROJECT_NAME} uses RadiumGltf extension") + # TODO : find why this find_package is needed (at least on MacOs whe ). + find_package(OpenMP QUIET) + find_package(RadiumGlTF REQUIRED) + target_compile_definitions(${PROJECT_NAME} PUBLIC USE_RADIUMGLTF) + target_link_libraries(${PROJECT_NAME} PUBLIC RadiumGlTF::RadiumGlTF) +endif() + +configure_radium_app(NAME ${PROJECT_NAME}) diff --git a/examples/DataflowExamples/GraphRendering/main.cpp b/examples/DataflowExamples/GraphRendering/main.cpp new file mode 100644 index 00000000000..7237434e0f6 --- /dev/null +++ b/examples/DataflowExamples/GraphRendering/main.cpp @@ -0,0 +1,385 @@ +// Include Radium base application and its simple Gui + +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +#ifdef USE_RADIUMGLTF +# include +# include +# include +# include +#endif + +// Qt Widgets +#include + +/* ----------------------------------------------------------------------------------- */ +using namespace Ra::Core::Utils; +using namespace Ra::Engine; +using namespace Ra::Gui; +using namespace Ra::Dataflow::Rendering::Renderer; +using namespace Ra::Dataflow::Rendering; + +/** + * Extending Ra::SimpleWindow with some menus for demonstration purpose + */ +class DemoWindowFactory : public BaseApplication::WindowFactory +{ + std::vector> m_renderers; + + static void addFileMenu( MainWindowInterface* window ) { + // Add a menu to load a scene + auto fileMenu = window->menuBar()->addMenu( "&File" ); + auto fileOpenAction = new QAction( "&Open...", window ); + fileOpenAction->setShortcuts( QKeySequence::Open ); + fileOpenAction->setStatusTip( "Open a file." ); + fileMenu->addAction( fileOpenAction ); + + // Connect the menu + auto openFile = [window]() { + QString filter; + QString allexts; + auto engine = RadiumEngine::getInstance(); + for ( const auto& loader : engine->getFileLoaders() ) { + QString exts; + for ( const auto& e : loader->getFileExtensions() ) { + exts.append( QString::fromStdString( e ) + " " ); + } + allexts.append( exts + " " ); + filter.append( QString::fromStdString( loader->name() ) + " (" + exts + ");;" ); + } + // add a filter concatenating all the supported extensions + filter.prepend( "Supported files (" + allexts + ");;" ); + + // remove the last ";;" of the string + filter.remove( filter.size() - 2, 2 ); + + QSettings settings; + auto path = settings.value( "files/load", QDir::homePath() ).toString(); + auto pathList = QFileDialog::getOpenFileNames( window, "Open Files", path, filter ); + + if ( !pathList.empty() ) { + engine->getEntityManager()->deleteEntities(); + settings.setValue( "files/load", pathList.front() ); + engine->loadFile( pathList.front().toStdString() ); + engine->releaseFile(); + window->prepareDisplay(); + emit window->getViewer()->needUpdate(); + } + }; + QAction::connect( fileOpenAction, &QAction::triggered, openFile ); + + // Add an exit entry + auto exitAct = fileMenu->addAction( "E&xit", window, &QWidget::close ); + exitAct->setShortcuts( QKeySequence::Quit ); + } + + static void + addRendererMenu( MainWindowInterface* window, + const std::vector>& renderers ) { + auto renderMenu = window->menuBar()->addMenu( "&Renderer" ); + int renderNum = 0; + + for ( const auto& rndr : renderers ) { + std::cout << "adding renderer : " << rndr->getRendererName() << "\n"; + + window->addRenderer( rndr->getRendererName(), rndr ); + auto rndAct = new QAction( rndr->getRendererName().c_str(), window ); + renderMenu->addAction( rndAct ); + QAction::connect( rndAct, &QAction::triggered, [renderNum, window]() { + window->getViewer()->changeRenderer( renderNum ); + // window->getViewer()->needUpdate(); + } ); + + // nodeToolBar* ff; + // build a toolbar for the renderers + if ( rndr->getRendererName() == "Custom Node Renderer" ) { + auto nodeToolBar = window->addToolBar( "NodeGraph control" ); + nodeToolBar->setObjectName( "Edit Graph" ); + auto grphAction = new QAction( "Edit Graph", window ); + nodeToolBar->addAction( grphAction ); + nodeToolBar->hide(); + auto* cnfgRndr = + dynamic_cast( + rndr.get() ); + + QAction::connect( grphAction, &QAction::triggered, [cnfgRndr, window]() { + auto& nbrCtrl = + dynamic_cast( + cnfgRndr->getController() ); + auto editor = new Ra::Dataflow::QtGui::GraphEditor::GraphEditorWindow( + nbrCtrl.getGraph() ); + Ra::Dataflow::QtGui::GraphEditor::GraphEditorWindow::connect( + editor, + &Ra::Dataflow::QtGui::GraphEditor::GraphEditorWindow::needUpdate, + [window]() { window->getViewer()->needUpdate(); } ); + editor->show(); + } ); + QAction::connect( + rndAct, &QAction::triggered, [nodeToolBar]() { nodeToolBar->show(); } ); + } + else { + QAction::connect( rndAct, &QAction::triggered, [window]() { + auto toolbars = window->findChildren(); + for ( auto tb : toolbars ) { + if ( tb->objectName() == "Edit Graph" ) { tb->hide(); } + } + } ); + } + + ++renderNum; + } + } + + public: + explicit DemoWindowFactory( + const std::vector>& renderers ) : + m_renderers( renderers ) {} + + inline Ra::Gui::MainWindowInterface* createMainWindow() const override { + auto window = new SimpleWindow(); +#ifdef USE_RADIUMGLTF + // register the gltf loader + std::shared_ptr loader = + std::make_shared(); + Ra::Engine::RadiumEngine::getInstance()->registerFileLoader( loader ); +#endif + addFileMenu( window ); + addRendererMenu( window, m_renderers ); + auto processPicking = + [viewer = window->getViewer()]( + const Ra::Engine::Rendering::Renderer::PickingResult& pickingResult ) { + if ( pickingResult.getRoIdx().isValid() ) { + std::cout << "Pick Ro number " << pickingResult.getRoIdx() << "\n"; + auto roManager = + Ra::Engine::RadiumEngine::getInstance()->getRenderObjectManager(); + auto editedRo = roManager->getRenderObject( pickingResult.getRoIdx() ); + auto pickedMaterial = editedRo->getMaterial(); + + // Update the viewer whenever a parameter gets modified + auto on_materialParametersModified = [editedRo, + viewer]( const std::string& nm ) { + if ( editedRo ) { + editedRo->getMaterial()->updateFromParameters(); + editedRo->setTransparent( editedRo->getMaterial()->isTransparent() ); + } + viewer->needUpdate(); + }; + std::cout << "Create materialEditor for Ro " << editedRo->getIndex() << " (" + << editedRo->getName() << ")\n"; + auto matParamsEditor = new Ra::Gui::MaterialParameterEditor; + matParamsEditor->setAttribute( Qt::WA_DeleteOnClose, true ); + QObject::connect( matParamsEditor, + &Ra::Gui::MaterialParameterEditor::materialParametersModified, + on_materialParametersModified ); + /* + QObject::connect( + matParamsEditor, &QObject::destroyed, [matParamsEditor, editedRo]() { + std::cout << "Destroying editor for Ro " << editedRo->getIndex() << " + (" + << editedRo->getName() << ")\n"; + } ); + */ + matParamsEditor->setupFromMaterial( pickedMaterial ); + matParamsEditor->show(); + } + }; + + QObject::connect( + window->getViewer(), &Ra::Gui::Viewer::rightClickPicking, processPicking ); + + return window; + } +}; +/* ----------------------------------------------------------------------------------- */ +// Renderer controller +#include +#include +#include + +#include + +class MyRendererController : public RenderGraphController +{ + private: + static void inspectGraph( DataflowGraph& g ) { + // Factories used by the graph + auto factories = g.getNodeFactories(); + std::cout << "Used factories by the graph \"" << g.getInstanceName() << "\" with type \"" + << g.getTypeName() << "\" :\n"; + for ( const auto& f : *( factories.get() ) ) { + std::cout << "\t" << f.first << "\n"; + } + + // Nodes of the graph + const auto& nodes = g.getNodes(); + std::cout << "Nodes of the graph " << g.getInstanceName() << " (" << nodes.size() + << ") :\n"; + for ( const auto& n : nodes ) { + std::cout << "\t\"" << n->getInstanceName() << "\" of type \"" << n->getTypeName() + << "\"\n"; + // Inspect input, output and interfaces of the node + std::cout << "\t\tInput ports :\n"; + for ( const auto& p : n->getInputs() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() + << "\n"; + } + std::cout << "\t\tOutput ports :\n"; + for ( const auto& p : n->getOutputs() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() + << "\n"; + } + std::cout << "\t\tInterface ports :\n"; + for ( const auto& p : n->getInterfaces() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() + << "\n"; + } + } + + // Nodes by level after the compilation + auto c = g.compile(); + if ( c ) { + const auto& cn = g.getNodesByLevel(); + std::cout << "Nodes of the graph, sorted by level when compiling the graph :\n"; + for ( size_t i = 0; i < cn.size(); ++i ) { + std::cout << "\tLevel " << i << " :\n"; + for ( const auto n : cn[i] ) { + std::cout << "\t\t\"" << n->getInstanceName() << "\"\n"; + } + } + + // describe the graph interface : inputs and outputs port of the whole graph (not of the + // nodes) + std::cout << "Inputs and output nodes of the graph " << g.getInstanceName() << " :\n"; + auto inputs = g.getAllDataSetters(); + std::cout << "\tInput ports (" << inputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : inputs ) { + std::cout << "\t\t\"" << portName << "\" accepting type \"" << portType << "\"\n"; + } + auto outputs = g.getAllDataGetters(); + std::cout << "\tOutput ports (" << outputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : outputs ) { + std::cout << "\t\t\"" << portName << "\" generating type \"" << portType << "\"\n"; + } + } + else { std::cerr << "Unable to compile the graph " << g.getInstanceName() << "\n"; } + } + + public: + using RenderGraphController::RenderGraphController; + + /// Configuration function. + /// Called once at the configuration of the renderer + /// If a graph should be loaded at configure time, it was set on the controller using + /// deferredLoadGraph(...) before configuring the renderer. + void configure( ControllableRenderer* renderer, int w, int h ) override { + LOG( logINFO ) << "MyRendererController::configure"; + RenderGraphController::configure( renderer, w, h ); + if ( m_renderGraph == nullptr ) { + // a graph was not given on the command line, build a simple one + m_renderGraph = std::make_unique( "Demonstration graph" ); + m_renderGraph->setShaderProgramManager( m_shaderMngr ); + auto sceneNode = new SceneNode( "Scene" ); + m_renderGraph->addNode( std::unique_ptr( sceneNode ) ); + auto resultNode = new DisplaySinkNode( "Images" ); + m_renderGraph->addNode( std::unique_ptr( resultNode ) ); + auto geomAovs = new GeometryAovsNode( "Geometry Aovs" ); + m_renderGraph->addNode( std::unique_ptr( geomAovs ) ); + auto simpleRenderNode = new SimpleRenderNode( "renderOperator" ); + m_renderGraph->addNode( std::unique_ptr( simpleRenderNode ) ); + bool linksOK = true; + linksOK = linksOK && + m_renderGraph->addLink( sceneNode, "objects", simpleRenderNode, "objects" ); + linksOK = linksOK && + m_renderGraph->addLink( sceneNode, "camera", simpleRenderNode, "camera" ); + linksOK = linksOK && + m_renderGraph->addLink( sceneNode, "lights", simpleRenderNode, "lights" ); + linksOK = linksOK && + m_renderGraph->addLink( simpleRenderNode, "Beauty", resultNode, "Beauty" ); + linksOK = linksOK && + m_renderGraph->addLink( simpleRenderNode, "Depth AOV", resultNode, "AOV_0" ); + linksOK = + linksOK && m_renderGraph->addLink( geomAovs, "world normal", resultNode, "AOV_1" ); + linksOK = linksOK && m_renderGraph->addLink( sceneNode, "camera", geomAovs, "camera" ); + linksOK = + linksOK && m_renderGraph->addLink( sceneNode, "objects", geomAovs, "objects" ); + + if ( !linksOK ) { LOG( logERROR ) << "Something went wrong when linking nodes !!! "; } + else { LOG( logINFO ) << "Graph linked successfully!!! "; } + + inspectGraph( *m_renderGraph ); + + // force recompilation and introspection of the graph by the renderer + m_renderGraph->needsRecompile(); + notify(); + } + }; + + [[nodiscard]] std::string getRendererName() const override { return "Custom Node Renderer"; } +}; + +/* ----------------------------------------------------------------------------------- */ + +/** + * main function. + */ +int main( int argc, char* argv[] ) { + + //! [Instatiating the application] + BaseApplication app( argc, argv ); + //! [Instatiating the application] + + //! getting graph argument on the command line + std::optional graphOption { std::nullopt }; + QCommandLineParser parser; + QCommandLineOption graphOpt( + { "g", "graph", "nodes" }, "Open a node at startup.", "graph", "" ); + parser.addOptions( { graphOpt } ); + if ( !parser.parse( app.arguments() ) ) { + LOG( Ra::Core::Utils::logWARNING ) + << "GraphDemo : Command line parsing failed due to unsupported or " + "missing options : \n\t" + << parser.errorText().toStdString(); + } + if ( parser.isSet( graphOpt ) ) { + graphOption = parser.value( graphOpt ).toStdString(); + std::cout << "Got a graph option : " << *graphOption << std::endl; + } + else { std::cout << "No graph option" << std::endl; } + //! getting graph argument on the command line + + //! [Initializing the application] + std::vector> renderers; + renderers.emplace_back( new Rendering::ForwardRenderer ); + + MyRendererController graphController; + if ( graphOption ) { graphController.deferredLoadGraph( *graphOption ); } + renderers.emplace_back( new ControllableRenderer( graphController ) ); + + app.initialize( DemoWindowFactory( renderers ) ); + app.setContinuousUpdate( false ); + app.addRadiumMenu(); +#ifdef USE_RADIUMGLTF + app.m_mainWindow->getViewer()->makeCurrent(); + // initialize the use of GLTF library + GLTF::initializeGltf(); + app.m_mainWindow->getViewer()->doneCurrent(); +#endif + //! [Initializing the application] + + return app.exec(); +} diff --git a/examples/DataflowExamples/GraphSerialization/CMakeLists.txt b/examples/DataflowExamples/GraphSerialization/CMakeLists.txt new file mode 100644 index 00000000000..fe1d256a40d --- /dev/null +++ b/examples/DataflowExamples/GraphSerialization/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0071 NEW) + +if(APPLE) + cmake_policy(SET CMP0042 NEW) +endif(APPLE) + +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +project(GraphSerialization VERSION 0.0.1) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Set default install location to installed- folder in build dir we do not want to +# install to /usr by default +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX + "${CMAKE_CURRENT_BINARY_DIR}/installed-${CMAKE_CXX_COMPILER_ID}-${CMAKE_BUILD_TYPE}" + CACHE PATH "Install path prefix, prepended onto install directories." FORCE + ) + message(STATUS "Set install prefix to ${CMAKE_INSTALL_PREFIX}") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# /////////////////////////////// +find_package(Radium REQUIRED COMPONENTS Dataflow) + +# -------------------------------------------------------------------------------------------------- + +set(app_sources main.cpp) +# set(app_headers) + +# delete ${app_headers} +add_executable(${PROJECT_NAME} ${app_sources}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) + +target_link_libraries(${PROJECT_NAME} PUBLIC Radium::Dataflow) + +configure_radium_app(NAME ${PROJECT_NAME}) diff --git a/examples/DataflowExamples/GraphSerialization/main.cpp b/examples/DataflowExamples/GraphSerialization/main.cpp new file mode 100644 index 00000000000..4d49e77f8f1 --- /dev/null +++ b/examples/DataflowExamples/GraphSerialization/main.cpp @@ -0,0 +1,174 @@ +#include +#include +#include +#include + +#include + +#include + +using namespace Ra::Dataflow::Core; + +/* ----------------------------------------------------------------------------------- */ +/** + * \brief Demonstrate how serialize a graph with custom nodes. + */ +int main( int argc, char* argv[] ) { + + //! [Creating the factory for the custom nodes and add it to the nodes system] + using VectorType = std::vector; + // using VectorType = Ra::Core::VectorArray; + // custom node type are either specialization of templated nodes or user-define nodes class + + // create the custom node factory + auto customFactory = NodeFactoriesManager::createFactory( "ExampleCustomFactory" ); + // add node creators to the factory + customFactory->registerNodeCreator>( + Sources::SingleDataSourceNode::getTypename() + "_", "Custom" ); + customFactory->registerNodeCreator>( + Functionals::FilterNode::getTypename() + "_", "Custom" ); + customFactory->registerNodeCreator>( + Sinks::SinkNode::getTypename() + "_", "Custom" ); + + //! [Creating the factory for the custom node types and add it to the node system] + + { + //! [Creating an empty graph using the custom nodes factory] + DataflowGraph g { "Serialization example" }; + // Add to the graph the custom factory (built-in nodes are automatically managed) + g.addFactory( NodeFactoriesManager::getFactory( "ExampleCustomFactory" ) ); + //! [Creating an empty graph using the custom nodes factory] + + //! [Creating Nodes] + auto sourceNode = new Sources::SingleDataSourceNode( "Source" ); + // non serializable node using a custom filter + auto filterNode = new Functionals::FilterNode( + "Filter", []( const Scalar& x ) { return x > 0.5_ra; } ); + auto sinkNode = new Sinks::SinkNode( "Sink" ); + //! [Creating Nodes] + + //! [Adding Nodes to the graph] + g.addNode( std::unique_ptr( sourceNode ) ); + g.addNode( std::unique_ptr( filterNode ) ); + g.addNode( std::unique_ptr( sinkNode ) ); + //! [Adding Nodes to the graph] + + //! [Creating links between Nodes] + g.addLink( sourceNode, "to", filterNode, "in" ); + g.addLink( filterNode, "out", sinkNode, "from" ); + //! [Creating links between Nodes] + + //! [Inspect the graph interface : inputs and outputs port] + auto inputs = g.getAllDataSetters(); + std::cout << "Input ports (" << inputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : inputs ) { + std::cout << "\t\"" << portName << "\" accepting type " << portType << "\n"; + } + auto outputs = g.getAllDataGetters(); + std::cout << "Output ports (" << outputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : outputs ) { + std::cout << "\t\"" << portName << "\" generating type " << portType << "\n"; + } + //! [Inspect the graph interface : inputs and outputs port] + + //! [Verifing the graph can be compiled] + if ( !g.compile() ) { + std::cout << " compilation failed"; + return 1; + } + //! [Verifing the graph can be compiled] + + //! [Serializing the graph] + g.saveToJson( "GraphSerializeExample.json" ); + //! [Serializing the graph] + } + + std::cout << "\t==**==**==*==**==*==**==*==**==*==**==*==**==*==**==\n"; + std::cout << "\t\tLoading and using the graph ...\n"; + std::cout << "\t==**==**==*==**==*==**==*==**==*==**==*==**==*==**==\n"; + + //! [Creating an empty graph and load it from a file] + DataflowGraph g1 { "" }; + g1.loadFromJson( "GraphSerializeExample.json" ); + //! [Creating an empty graph and load it from a file] + + //! [Inspect the graph interface : inputs and outputs port] + auto inputs_g1 = g1.getAllDataSetters(); + std::cout << "Input ports (" << inputs_g1.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : inputs_g1 ) { + std::cout << "\t\"" << portName << "\" accepting type " << portType << "\n"; + } + auto outputs_g1 = g1.getAllDataGetters(); + std::cout << "Output ports (" << outputs_g1.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : outputs_g1 ) { + std::cout << "\t\"" << portName << "\" generating type " << portType << "\n"; + } + //! [Inspect the graph interface : inputs and outputs port] + + //! [Verifing the graph can be compiled] + if ( !g1.compile() ) { + std::cout << "Compilation failed for the loaded graph"; + return 2; + } + //! [Verifing the graph can be compiled] + + //! [Creating input variable to test the graph] + VectorType test; + test.reserve( 10 ); + std::mt19937 gen( 0 ); + std::uniform_real_distribution<> dis( 0.0, 1.0 ); + // Fill the vector with random numbers between 0 and 1 + for ( int n = 0; n < test.capacity(); ++n ) { + test.push_back( dis( gen ) ); + } + + std::cout << "Input values : \n\t"; + for ( auto ord : test ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Creating input variable to test the graph] + + //! [setting the values processed by the graph] + auto input = g1.getDataSetter( "Source_to" ); + input->setData( &test ); + //! [setting the values processed by the graph] + + //! [Execute the graph] + std::cout << "Executing the loaded graph ...\n"; + g1.execute(); + //! [Execute the graph] + + //! [Print the output result] + auto output = g1.getDataGetter( "Sink_from" ); + VectorType result; + output->getData( result ); + std::cout << "Output values : \n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Print the output result] + + //! [Set the correct filter on the filter node] + auto filter = dynamic_cast*>( g1.getNode( "Filter" ) ); + if ( !filter ) { + std::cerr << "Unable to cast the filter to the right type\n"; + return 3; + } + filter->setFilterFunction( []( const Scalar& f ) { return f > 0.5_ra; } ); + //! [Set the correct filter on the filter node] + + //! [Execute the graph] + std::cout << "Executing the re-parameterized graph ...\n"; + g1.execute(); + output->getData( result ); + std::cout << "Output values : \n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Execute the graph] + + return 0; +} diff --git a/examples/DataflowExamples/HelloGraph/CMakeLists.txt b/examples/DataflowExamples/HelloGraph/CMakeLists.txt new file mode 100644 index 00000000000..6d2ab47e0ae --- /dev/null +++ b/examples/DataflowExamples/HelloGraph/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.18) +cmake_policy(SET CMP0071 NEW) + +if(APPLE) + cmake_policy(SET CMP0042 NEW) +endif(APPLE) + +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +project(HelloGraph VERSION 0.0.1) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Set default install location to installed- folder in build dir we do not want to +# install to /usr by default +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX + "${CMAKE_CURRENT_BINARY_DIR}/installed-${CMAKE_CXX_COMPILER_ID}-${CMAKE_BUILD_TYPE}" + CACHE PATH "Install path prefix, prepended onto install directories." FORCE + ) + message(STATUS "Set install prefix to ${CMAKE_INSTALL_PREFIX}") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# /////////////////////////////// +find_package(Radium REQUIRED COMPONENTS Dataflow) + +# -------------------------------------------------------------------------------------------------- + +set(app_sources main.cpp) +# set(app_headers) + +# delete ${app_headers} +add_executable(${PROJECT_NAME} ${app_sources}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) + +target_link_libraries(${PROJECT_NAME} PUBLIC Radium::Dataflow) + +configure_radium_app(NAME ${PROJECT_NAME}) diff --git a/examples/DataflowExamples/HelloGraph/main.cpp b/examples/DataflowExamples/HelloGraph/main.cpp new file mode 100644 index 00000000000..fdceb6aa4f4 --- /dev/null +++ b/examples/DataflowExamples/HelloGraph/main.cpp @@ -0,0 +1,151 @@ +#include +#include +#include +#include + +using namespace Ra::Dataflow::Core; + +/* ----------------------------------------------------------------------------------- */ +/** + * \brief Demonstrate how to use a graph to filter a collection + */ +int main( int argc, char* argv[] ) { + using RaVector = Ra::Core::VectorArray; + //! [Creating an empty graph] + DataflowGraph g { "helloGraph" }; + //! [Creating an empty graph] + + //! [Creating Nodes] + auto addedSourceNode = std::make_unique>( "Source" ); + auto addedPredicateNode = std::make_unique( "Selector" ); + auto addedFilterNode = std::make_unique>( "Filter" ); + auto addedSinkNode = std::make_unique>( "Sink" ); + // get non-owning aliases to ease future node management + auto sourceNode = addedSourceNode.get(); + auto predicateNode = addedPredicateNode.get(); + auto filterNode = addedFilterNode.get(); + auto sinkNode = addedSinkNode.get(); + //! [Creating Nodes] + + //! [Adding Nodes to the graph] + g.addNode( std::move( addedSourceNode ) ); + g.addNode( std::move( addedPredicateNode ) ); + g.addNode( std::move( addedFilterNode ) ); + g.addNode( std::move( addedSinkNode ) ); + //! [Adding Nodes to the graph] + + //! [Creating links between Nodes] + g.addLink( sourceNode, "to", filterNode, "in" ); + if ( !g.addLink( predicateNode, "f", filterNode, "f" ) ) { + std::cerr << "Error, can't link functional ports !\n"; + std::abort(); + } + g.addLink( filterNode, "out", sinkNode, "from" ); + //! [Creating links between Nodes] + + //! [Inspect the graph interface : inputs and outputs port] + auto inputs = g.getAllDataSetters(); + std::cout << "Input ports (" << inputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : inputs ) { + std::cout << "\t\"" << portName << "\" accepting type " << portType << "\n"; + } + auto outputs = g.getAllDataGetters(); + std::cout << "Output ports (" << outputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : outputs ) { + std::cout << "\t\"" << portName << "\" generating type " << portType << "\n"; + } + //! [Inspect the graph interface : inputs and outputs port] + + //! [Verifying the graph can be compiled] + if ( !g.compile() ) { + std::cout << " compilation failed"; + return 1; + } + //! [Verifying the graph can be compiled] + + g.saveToJson( "illustrateDoc.json" ); + + ///! [Configure the interface ports (input and output of the graph)] + auto input = g.getDataSetter( "Source_to" ); + RaVector test; + input->setData( &test ); + + auto selector = g.getDataSetter( "Selector_f" ); + Sources::ScalarUnaryPredicateSource::function_type pred = []( Scalar x ) { return x < 0.5; }; + selector->setData( &pred ); + + auto output = g.getDataGetter( "Sink_from" ); + // The reference to the result will not be available before the first run + // auto& result = output->getData>(); + ///! [Configure the interface ports (input and output of the graph)] + + //! [Initializing input variable to test the graph] + test.reserve( 10 ); + std::mt19937 gen( 0 ); + std::uniform_real_distribution<> dis( 0.0, 1.0 ); + // Fill the vector with random numbers between 0 and 1 + for ( int n = 0; n < test.capacity(); ++n ) { + test.push_back( dis( gen ) ); + } + + std::cout << "Input values : \n\t"; + for ( auto ord : test ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Initializing input variable to test the graph] + + //! [Execute the graph] + g.execute(); + // The reference to the result is now available. Results can be accessed through a reference + // (sort of RVO from the graph https://en.cppreference.com/w/cpp/language/copy_elision) + // or copied in an application variable. + auto& result = output->getData(); + //! [Execute the graph] + + //! [Print the output result] + std::cout << "Output values (reference): " << result.size() << "\n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Print the output result] + + //! [Modify input and rerun the graph] + Sources::ScalarUnaryPredicateSource::function_type predbig = []( Scalar x ) { return x > 0.5; }; + selector->setData( &predbig ); + g.execute(); + std::cout << "Output values after second execution: " << result.size() << "\n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Modify input and rerun the graph] + + //! [Disconnect data setter and rerun the graph - result is now empty] + g.releaseDataSetter( "Source_to" ); + g.execute(); + std::cout << "Output values after third execution: " << result.size() << "\n"; + //! [Disconnect data setter and rerun the graph - result is now empty] + + //! [As interface is disconnected, we can set data direclty on the source node] + sourceNode->setData( &test ); + g.execute(); + std::cout << "Output values after fourth execution: " << result.size() << "\n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [As interface is disconnected, we can set data direclty on the source node] + + //! [Reconnect data setter and rerun the graph - result is the same than second execution] + g.activateDataSetter( "Source_to" ); + g.execute(); + std::cout << "Output values after last execution: " << result.size() << "\n\t"; + for ( auto ord : result ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + //! [Reconnect data setter and rerun the graph - result is the same than second execution] + return 0; +} diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index a09f9af4099..7b06fe131fe 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -101,6 +101,8 @@ else() add_subdirectory(IO) # add Gui dependencies add_subdirectory(Gui) + # add Dataflow dependencies + add_subdirectory(Dataflow) # generate radium-option.cmake configuration file file(REMOVE "${CMAKE_INSTALL_PREFIX}/radium-options.cmake") diff --git a/external/Core/CMakeLists.txt b/external/Core/CMakeLists.txt index 06fd9589787..21ce5678226 100644 --- a/external/Core/CMakeLists.txt +++ b/external/Core/CMakeLists.txt @@ -19,7 +19,7 @@ add_custom_target(CoreExternals ALL) if(NOT DEFINED Eigen3_DIR) check_externals_prerequisite() - status_message("" "eigen3" "remote git") + status_message("[CoreExternal]" "eigen3" "remote git") ExternalProject_Add( Eigen3 @@ -41,7 +41,7 @@ endif() if(NOT DEFINED OpenMesh_DIR) check_externals_prerequisite() - status_message("" "OpenMesh" "remote git") + status_message("[CoreExternal]" "OpenMesh" "remote git") # I found some problems when generating xcode project for OpenMesh: the libXXX.dylib link to # libXXX.Major.minor.dylib (eg libOpenMesh.2.1.dylib) is not generated and the script failed. # Need to generate this link manually. TODO, find why only OpenMesh is problematic @@ -63,7 +63,7 @@ endif() if(NOT DEFINED cpplocate_DIR OR NOT cpplocate_DIR) check_externals_prerequisite() - status_message("" "cpplocate" "remote git") + status_message("[CoreExternal]" "cpplocate" "remote git") ExternalProject_Add( cpplocate GIT_REPOSITORY https://github.com/cginternals/cpplocate.git diff --git a/external/Dataflow/CMakeLists.txt b/external/Dataflow/CMakeLists.txt new file mode 100644 index 00000000000..b991d7c59d2 --- /dev/null +++ b/external/Dataflow/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.6) + +project(radiumdataflow-external VERSION 1.0.0) + +include(ExternalProject) +include(ExternalInclude) + +list(APPEND CMAKE_MESSAGE_INDENT "[Dataflow] ") +string(REPLACE ";" "" indent_string "${CMAKE_MESSAGE_INDENT}") +set(indent_string "${indent_string}--") + +# force installing by default all the external projects +set_property(DIRECTORY PROPERTY EP_STEP_TARGETS install) + +# Add fPIC for all dependencies +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +add_custom_target(DataflowExternals ALL) + +if(NOT DEFINED RadiumNodeEditor_DIR) + check_externals_prerequisite() + status_message("" "RadiumNodeEditor" "remote git") + ExternalProject_Add( + RadiumNodeEditor + GIT_REPOSITORY https://github.com/MathiasPaulin/RadiumQtNodeEditor.git + GIT_TAG main + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + INSTALL_DIR "${CMAKE_INSTALL_PREFIX}" + CMAKE_ARGS ${RADIUM_EXTERNAL_CMAKE_OPTIONS} -DCMAKE_INSTALL_PREFIX= + "-DCMAKE_MESSAGE_INDENT=${indent_string}\;" + ) + set_external_dir(RadiumNodeEditor "lib/cmake/RadiumNodeEditor/") + add_dependencies(DataflowExternals RadiumNodeEditor) +else() + status_message("" "RadiumNodeEditor" ${RadiumNodeEditor_DIR}) +endif() diff --git a/scripts/generateFilelistForModule.sh b/scripts/generateFilelistForModule.sh index 9448c419fac..2e3caa7336f 100755 --- a/scripts/generateFilelistForModule.sh +++ b/scripts/generateFilelistForModule.sh @@ -7,12 +7,16 @@ fi BASE=$1 LOWBASE=$(echo "$BASE" | tr '[:upper:]' '[:lower:]') +LOWBASE=$(echo "$LOWBASE" | tr '/' '_') OUTPUT="../src/${BASE}/filelist.cmake" echo "generate [${BASE}] filelist" echo "-- input files from [../src/${BASE}]" echo "-- output vars prefix [${LOWBASE}]" echo "-- output file is [${OUTPUT}]" +DELIM='/' +SLASHES=$( awk -F"$DELIM" '{print NF-1}' <<<"${BASE}" ) +(( SLASHES = 4 + SLASHES )) rm -f "${OUTPUT}" @@ -32,7 +36,7 @@ function genList(){ if [ ! -z "$L" ] then echo "set(${LOWBASE}_${suffix}" >> "${OUTPUT}" - echo "${L}" | cut -f 4- -d/ | grep -v pch.hpp | sort | xargs -n1 echo " " >> "${OUTPUT}" + echo "${L}" | cut -f $SLASHES- -d/ | grep -v pch.hpp | sort | xargs -n1 echo " " >> "${OUTPUT}" echo ")" >> "${OUTPUT}" echo "" >> "${OUTPUT}" fi diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5ff89b416a7..50b71e8094a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,7 +31,7 @@ set(CONFIG_PACKAGE_LOCATION lib/cmake/Radium) # ----------------------------------------------------------------------------------- # Include sources and declare components -foreach(lib Core Engine IO PluginBase Gui Headless) +foreach(lib Core Engine IO PluginBase Gui Headless Dataflow) string(TOUPPER ${lib} upcase_lib) if(RADIUM_GENERATE_LIB_${upcase_lib}) add_subdirectory(${lib}) diff --git a/src/Core/Random/RandomPointSet.cpp b/src/Core/Random/RandomPointSet.cpp new file mode 100644 index 00000000000..1529c8de6d9 --- /dev/null +++ b/src/Core/Random/RandomPointSet.cpp @@ -0,0 +1,56 @@ +#include + +namespace Ra { +namespace Core { +namespace Random { + +FibonacciSequence::FibonacciSequence( size_t number ) : n { std::max( size_t( 5 ), number ) } {} + +size_t FibonacciSequence::range() { + return n; +} +Scalar FibonacciSequence::operator()( size_t i ) { + return Scalar( i ) / phi; +} + +Scalar VanDerCorputSequence::operator()( unsigned int bits ) { + bits = ( bits << 16u ) | ( bits >> 16u ); + bits = ( ( bits & 0x55555555u ) << 1u ) | ( ( bits & 0xAAAAAAAAu ) >> 1u ); + bits = ( ( bits & 0x33333333u ) << 2u ) | ( ( bits & 0xCCCCCCCCu ) >> 2u ); + bits = ( ( bits & 0x0F0F0F0Fu ) << 4u ) | ( ( bits & 0xF0F0F0F0u ) >> 4u ); + bits = ( ( bits & 0x00FF00FFu ) << 8u ) | ( ( bits & 0xFF00FF00u ) >> 8u ); + return Scalar( float( bits ) * 2.3283064365386963e-10_ra ); // / 0x100000000 +} + +FibonacciPointSet::FibonacciPointSet( size_t n ) : seq( n ) {} + +size_t FibonacciPointSet::range() { + return seq.range(); +} +Ra::Core::Vector2 FibonacciPointSet::operator()( size_t i ) { + return { seq( i ), Scalar( i ) / Scalar( range() ) }; +} + +HammersleyPointSet::HammersleyPointSet( size_t number ) : n( number ) {} + +size_t HammersleyPointSet::range() { + return n; +} + +Ra::Core::Vector2 HammersleyPointSet::operator()( size_t i ) { + return { Scalar( i ) / Scalar( range() ), seq( i ) }; +} + +MersenneTwisterPointSet::MersenneTwisterPointSet( size_t number ) : + gen( 0 ), seq( 0._ra, 1._ra ), n( number ) {} + +size_t MersenneTwisterPointSet::range() { + return n; +} +Ra::Core::Vector2 MersenneTwisterPointSet::operator()( size_t ) { + return { seq( gen ), seq( gen ) }; +} + +} // namespace Random +} // namespace Core +} // namespace Ra diff --git a/src/Core/Random/RandomPointSet.hpp b/src/Core/Random/RandomPointSet.hpp new file mode 100644 index 00000000000..d79f620f98c --- /dev/null +++ b/src/Core/Random/RandomPointSet.hpp @@ -0,0 +1,180 @@ +#pragma once +#include + +#include + +#include + +namespace Ra { +namespace Core { + +/** \brief Random point set utilities. + * + * This namespace contains functions, classes and utilities for 1D, 2D and 3D random point sets + * management. + * + */ +namespace Random { + +/** + * \brief Defines some helper constexpr functions + */ +namespace internal { +Scalar constexpr sqrtNewtonRaphsonHelper( Scalar x, Scalar curr, Scalar prev ) { + return curr == prev ? curr : sqrtNewtonRaphsonHelper( x, 0.5_ra * ( curr + x / curr ), curr ); +} +Scalar constexpr sqrtConstExpr( Scalar x ) { + return sqrtNewtonRaphsonHelper( x, x, 0_ra ); +} +} // namespace internal + +/** \brief Implements the fibonacci sequence + * i --> i/phi + * where phi = (1 + sqrt(5)) / 2 + */ +class RA_CORE_API FibonacciSequence +{ + + static constexpr Scalar phi = ( 1_ra + internal::sqrtConstExpr( 5_ra ) ) / 2_ra; + size_t n; + + public: + explicit FibonacciSequence( size_t number ); + // copyable + FibonacciSequence( const FibonacciSequence& ) = default; + FibonacciSequence& operator=( const FibonacciSequence& ) = default; + // movable + FibonacciSequence( FibonacciSequence&& ) = default; + FibonacciSequence& operator=( FibonacciSequence&& ) = default; + virtual ~FibonacciSequence() = default; + + size_t range(); + Scalar operator()( size_t i ); +}; + +/** \brief 1D Van der Corput sequence + * only implemented for 32bits floats (converted out to Scalar) + */ +struct RA_CORE_API VanDerCorputSequence { + Scalar operator()( unsigned int bits ); +}; + +/** \brief Implements the 2D fibonacci Point set + * points follow the FibonacciSequence + * (i, N) => [i / phi, i / N] + */ +class RA_CORE_API FibonacciPointSet +{ + FibonacciSequence seq; + + public: + explicit FibonacciPointSet( size_t n ); + // copyable + FibonacciPointSet( const FibonacciPointSet& ) = default; + FibonacciPointSet& operator=( const FibonacciPointSet& ) = default; + // movable + FibonacciPointSet( FibonacciPointSet&& ) = default; + FibonacciPointSet& operator=( FibonacciPointSet&& ) = default; + virtual ~FibonacciPointSet() = default; + + size_t range(); + Ra::Core::Vector2 operator()( size_t i ); +}; + +/** \brief 2D Hammersley point set + * + */ +class RA_CORE_API HammersleyPointSet +{ + VanDerCorputSequence seq; + size_t n; + + public: + explicit HammersleyPointSet( size_t number ); + // copyable + HammersleyPointSet( const HammersleyPointSet& ) = default; + HammersleyPointSet& operator=( const HammersleyPointSet& ) = default; + // movable + HammersleyPointSet( HammersleyPointSet&& ) = default; + HammersleyPointSet& operator=( HammersleyPointSet&& ) = default; + virtual ~HammersleyPointSet() = default; + + size_t range(); + Ra::Core::Vector2 operator()( size_t i ); +}; + +/** \brief 2D Random point set + */ +class RA_CORE_API MersenneTwisterPointSet +{ + std::mt19937 gen; + std::uniform_real_distribution seq; + size_t n; + + public: + explicit MersenneTwisterPointSet( size_t number ); + // copyable + MersenneTwisterPointSet( const MersenneTwisterPointSet& ) = default; + MersenneTwisterPointSet& operator=( const MersenneTwisterPointSet& ) = default; + // movable + MersenneTwisterPointSet( MersenneTwisterPointSet&& ) = default; + MersenneTwisterPointSet& operator=( MersenneTwisterPointSet&& ) = default; + virtual ~MersenneTwisterPointSet() = default; + + size_t range(); + Ra::Core::Vector2 operator()( size_t ); +}; + +// https://observablehq.com/@mbostock/spherical-fibonacci-lattice +// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html +/** \brief Map a [0, 1)^2 point set on the unit sphere + * \tparam PointSet the random point set type to map on sphere + */ +template +class SphericalPointSet +{ + PointSet p; + Ra::Core::Vector3 projectOnSphere( const Ra::Core::Vector2&& pt ); + + public: + explicit SphericalPointSet( size_t n ); + // copyable + SphericalPointSet( const SphericalPointSet& ) = default; + SphericalPointSet& operator=( const SphericalPointSet& ) = default; + // movable + SphericalPointSet( SphericalPointSet&& ) = default; + SphericalPointSet& operator=( SphericalPointSet&& ) = default; + virtual ~SphericalPointSet() = default; + + size_t range(); + Ra::Core::Vector3 operator()( size_t i ); +}; + +// ------------------------------------------------------------------------ +// ------------------------ inline methods -------------------------------- + +template +Ra::Core::Vector3 SphericalPointSet::projectOnSphere( const Ra::Core::Vector2&& pt ) { + Scalar theta = std::acos( 2 * pt[1] - 1 ); // 0 <= tetha <= pi + Scalar phi = 2_ra * Scalar( M_PI ) * pt[0]; + return { std::sin( theta ) * std::cos( phi ), + std::sin( theta ) * std::sin( phi ), + std::cos( theta ) }; +} + +template +SphericalPointSet::SphericalPointSet( size_t n ) : p( n ) {} + +template +size_t SphericalPointSet::range() { + return p.range(); +} + +template +Ra::Core::Vector3 SphericalPointSet::operator()( size_t i ) { + return projectOnSphere( p( i ) ); +} + +} // namespace Random +} // namespace Core +} // namespace Ra diff --git a/src/Core/Utils/TypesUtils.hpp b/src/Core/Utils/TypesUtils.hpp index ac3d962d9e3..6305cad088f 100644 --- a/src/Core/Utils/TypesUtils.hpp +++ b/src/Core/Utils/TypesUtils.hpp @@ -1,6 +1,5 @@ #pragma once - -#include +#include #ifndef _WIN32 # include @@ -10,6 +9,7 @@ #endif #include +#include #include @@ -19,11 +19,40 @@ namespace Utils { /// Return the human readable version of the type name T template -const char* demangleType() noexcept; +std::string demangleType() noexcept; /// Return the human readable version of the given object's type template -const char* demangleType( const T& ) noexcept; +std::string demangleType( const T& ) noexcept; + +/// Return the human readable version of the given type name +std::string demangleType( const std::type_index& typeIndex ) noexcept; + +// Check if a type is a container with access to its element type and number +// adapted from https://stackoverflow.com/questions/13830158/check-if-a-variable-type-is-iterable +namespace detail { + +using std::begin; +using std::end; + +template +auto is_container_impl( int ) + -> decltype( begin( std::declval() ) != + end( std::declval() ), // begin/end and operator != + void(), // Handle evil operator , + std::declval().empty(), + std::declval().size(), + ++std::declval() ) )&>(), // operator ++ + void( *begin( std::declval() ) ), // operator* + std::true_type {} ); + +template +std::false_type is_container_impl( ... ); + +} // namespace detail + +template +using is_container = decltype( detail::is_container_impl( 0 ) ); // TypeList taken and adapted from // https://github.com/AcademySoftwareFoundation/openvdb/blob/master/openvdb/openvdb/TypeList.h @@ -89,55 +118,49 @@ struct TypeList { }; #ifdef _WIN32 -// On windows (since MSVC 2019), typeid( T ).name() returns the demangled name -template -const char* demangleType() noexcept { - static auto demangled_name = []() { - std::string retval { typeid( T ).name() }; - removeAllInString( retval, "class " ); - removeAllInString( retval, "struct " ); - replaceAllInString( retval, ",", ", " ); - replaceAllInString( retval, "> >", ">>" ); - return retval; - }(); - - return demangled_name.data(); +// On windows (since MSVC 2019), typeid( T ).name() (and then typeIndex.name() returns the demangled +// name +inline std::string demangleType( const std::type_index& typeIndex ) noexcept { + std::string retval = typeIndex.name(); + removeAllInString( retval, "class " ); + removeAllInString( retval, "struct " ); + removeAllInString( retval, "__cdecl" ); + replaceAllInString( retval, "& __ptr64", "&" ); + replaceAllInString( retval, ",", ", " ); + replaceAllInString( retval, " >", ">" ); + replaceAllInString( retval, "__int64", "long" ); + replaceAllInString( retval, "const &", "const&" ); + return retval; } #else +// On Linux/macos, use the C++ ABI demangler +inline std::string demangleType( const std::type_index& typeIndex ) noexcept { + int error = 0; + std::string retval; + char* name = abi::__cxa_demangle( typeIndex.name(), 0, 0, &error ); + if ( error == 0 ) { retval = name; } + else { + // error : -1 --> memory allocation failed + // error : -2 --> not a valid mangled name + // error : other --> __cxa_demangle + retval = std::string( "Type demangler error : " ) + std::to_string( error ); + } + std::free( name ); + removeAllInString( retval, "__1::" ); // or "::__1" ? + replaceAllInString( retval, " >", ">" ); + return retval; +} +#endif template -const char* demangleType() noexcept { +std::string demangleType() noexcept { // once per one type - static auto demangled_name = []() { - int error = 0; - std::string retval; - char* name = abi::__cxa_demangle( typeid( T ).name(), 0, 0, &error ); - - switch ( error ) { - case 0: - retval = name; - break; - case -1: - retval = "memory allocation failed"; - break; - case -2: - retval = "not a valid mangled name"; - break; - default: - retval = "__cxa_demangle failed"; - break; - } - std::free( name ); - removeAllInString( retval, "__1::" ); // or "::__1" ? - replaceAllInString( retval, "> >", ">>" ); - return retval; - }(); - - return demangled_name.data(); + static auto demangled_name = demangleType( std::type_index( typeid( T ) ) ); + return demangled_name; } -#endif + // calling with instances template -const char* demangleType( const T& ) noexcept { +std::string demangleType( const T& ) noexcept { return demangleType(); } diff --git a/src/Core/filelist.cmake b/src/Core/filelist.cmake index 7aabbe1b640..4760bf220f9 100644 --- a/src/Core/filelist.cmake +++ b/src/Core/filelist.cmake @@ -35,6 +35,7 @@ set(core_sources Geometry/TriangleMesh.cpp Geometry/Volume.cpp Geometry/deprecated/TopologicalMesh.cpp + Random/RandomPointSet.cpp Resources/Resources.cpp Tasks/TaskQueue.cpp Utils/Attribs.cpp @@ -107,6 +108,7 @@ set(core_headers Math/Math.hpp Math/Quadric.hpp RaCore.hpp + Random/RandomPointSet.hpp Resources/Resources.hpp Tasks/Task.hpp Tasks/TaskQueue.hpp diff --git a/src/Dataflow/CMakeLists.txt b/src/Dataflow/CMakeLists.txt new file mode 100644 index 00000000000..2360159307a --- /dev/null +++ b/src/Dataflow/CMakeLists.txt @@ -0,0 +1,37 @@ +set(ra_dataflow_target Dataflow) +list(APPEND CMAKE_MESSAGE_INDENT "[${ra_dataflow_target}] ") + +project(${ra_dataflow_target} LANGUAGES CXX VERSION ${Radium_VERSION}) + +set(dataflow_headers RaDataflow.hpp) +add_library(${ra_dataflow_target} INTERFACE) + +if(RADIUM_GENERATE_LIB_CORE) + add_subdirectory(Core) + add_dependencies(${ra_dataflow_target} DataflowCore) + target_link_libraries(${ra_dataflow_target} INTERFACE DataflowCore) +endif() + +if(RADIUM_GENERATE_LIB_ENGINE) + add_subdirectory(Rendering) + add_dependencies(${ra_dataflow_target} DataflowRendering) + target_link_libraries(${ra_dataflow_target} INTERFACE DataflowRendering) +endif() + +if(RADIUM_GENERATE_LIB_GUI) + add_subdirectory(QtGui) + add_dependencies(${ra_dataflow_target} DataflowQtGui) + target_link_libraries(${ra_dataflow_target} INTERFACE DataflowQtGui) +endif() + +message(STATUS "Configuring library ${ra_dataflow_target} with standard settings") +target_include_directories( + ${ra_dataflow_target} INTERFACE $ + $ +) +configure_radium_library( + TARGET ${ra_dataflow_target} COMPONENT + PACKAGE_CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in FILES "${dataflow_headers}" +) +set(RADIUM_COMPONENTS ${RADIUM_COMPONENTS} ${ra_dataflow_target} PARENT_SCOPE) +set(RADIUM_MISSING_COMPONENTS ${RADIUM_MISSING_COMPONENTS} PARENT_SCOPE) diff --git a/src/Dataflow/Config.cmake.in b/src/Dataflow/Config.cmake.in new file mode 100644 index 00000000000..1df6db7e0d7 --- /dev/null +++ b/src/Dataflow/Config.cmake.in @@ -0,0 +1,44 @@ +# -------------- Configuration of the Radium Dataflow targets and definitions ----------------------- +# Setup Dataflow and check for dependencies +if (Dataflow_FOUND AND NOT TARGET Dataflow) + set(Configure_Dataflow ON) + # verify dependencies + if(NOT DataflowCore_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake") + set(DataflowCore_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::Dataflow: dependency DataflowCore not found") + set(Configure_Dataflow OFF) + endif() + endif() + + if(NOT DataflowQtGui_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../DataflowQtGui/RadiumDataflowQtGuiConfig.cmake") + set(DataflowQtGui_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../DataflowQtGui/RadiumDataflowQtGuiConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::Dataflow: dependency DataflowQtGui not found") + set(Configure_Dataflow OFF) + endif() + endif() + +# to be uncommented when dataflow rendering subpackage will be available + if(NOT DataflowRendering_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../DataflowRendering/RadiumDataflowRenderingConfig.cmake") + set(DataflowRendering_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../DataflowRendering/RadiumDataflowRenderingConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::Dataflow: dependency DataflowRendering not found") + set(Configure_Dataflow OFF) + endif() + endif() +endif() + +# configure Dataflow component +if(Configure_Dataflow) + include("${CMAKE_CURRENT_LIST_DIR}/DataflowTargets.cmake") +endif() diff --git a/src/Dataflow/Core/CMakeLists.txt b/src/Dataflow/Core/CMakeLists.txt new file mode 100644 index 00000000000..4db4eb2428c --- /dev/null +++ b/src/Dataflow/Core/CMakeLists.txt @@ -0,0 +1,43 @@ +set(ra_dataflowcore_target DataflowCore) +list(APPEND CMAKE_MESSAGE_INDENT "[${ra_dataflowcore_target}] ") + +project(${ra_dataflowcore_target} LANGUAGES CXX VERSION ${Radium_VERSION}) + +include(filelist.cmake) + +add_library( + ${ra_dataflowcore_target} SHARED ${dataflow_core_sources} ${dataflow_core_headers} + ${dataflow_core_private} +) + +# This one should be extracted directly from parent project properties. +target_compile_definitions(${ra_dataflowcore_target} PRIVATE RA_DATAFLOW_EXPORTS) + +target_compile_options(${ra_dataflowcore_target} PUBLIC ${RA_DEFAULT_COMPILE_OPTIONS}) +if("${CMAKE_CXX_COMPILER_ID}" MATCHES "MSVC") + target_compile_options(${ra_dataflowcore_target} PRIVATE /bigobj) +endif() + +add_dependencies(${ra_dataflowcore_target} Core) +target_link_libraries(${ra_dataflowcore_target} PUBLIC Core) + +message(STATUS "Configuring library ${ra_dataflowcore_target} with standard settings") +configure_radium_target(${ra_dataflowcore_target}) +# configure the library only. The package is a sub-package and should be configured independently +configure_radium_library( + TARGET ${ra_dataflowcore_target} COMPONENT TARGET_DIR "Dataflow/Core" + FILES "${dataflow_core_headers}" +) +# Generate cmake package +configure_radium_package( + NAME ${ra_dataflowcore_target} PACKAGE_CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in + PACKAGE_DIR "lib/cmake/Radium/${ra_dataflowcore_target}" NAME_PREFIX "Radium" +) + +set(RADIUM_COMPONENTS ${RADIUM_COMPONENTS} ${ra_dataflowcore_target} PARENT_SCOPE) + +if(RADIUM_ENABLE_PCH) + target_precompile_headers(${ra_dataflowcore_target} PRIVATE pch.hpp) +endif() + +list(REMOVE_AT CMAKE_MESSAGE_INDENT -1) diff --git a/src/Dataflow/Core/Config.cmake.in b/src/Dataflow/Core/Config.cmake.in new file mode 100644 index 00000000000..200aa5af5f9 --- /dev/null +++ b/src/Dataflow/Core/Config.cmake.in @@ -0,0 +1,21 @@ +# -------------- Configuration of the Radium DataflowCore targets and definitions ----------------------- +# Setup Engine and check for dependencies + +if (DataflowCore_FOUND AND NOT TARGET DataflowCore) + set(Configure_DataflowCore ON) + # verify dependencies + if(NOT Core_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../Core/RadiumCoreConfig.cmake") + set(Core_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../Core/RadiumCoreConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::DataflowCore: dependency Core not found") + set(Configure_DataflowCore OFF) + endif() + endif() +endif() + +if(Configure_DataflowCore) + include("${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Core/DataflowCoreTargets.cmake") +endif() diff --git a/src/Dataflow/Core/DataflowGraph.cpp b/src/Dataflow/Core/DataflowGraph.cpp new file mode 100644 index 00000000000..5a39fe1185c --- /dev/null +++ b/src/Dataflow/Core/DataflowGraph.cpp @@ -0,0 +1,698 @@ +#include +#include + +#include +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +using namespace Ra::Core::Utils; + +DataflowGraph::DataflowGraph( const std::string& name ) : DataflowGraph( name, getTypename() ) {} + +DataflowGraph::DataflowGraph( const std::string& instanceName, const std::string& typeName ) : + Node( instanceName, typeName ) { + // This will allow to edit subgraph in an independent editor + addEditableParameter( new EditableParameter( instanceName, *this ) ); + // A graph always use the builtin nodes factory + addFactory( NodeFactoriesManager::getDataFlowBuiltInsFactory() ); +} + +void DataflowGraph::init() { + if ( m_ready ) { + Node::init(); + std::for_each( m_nodesByLevel.begin(), m_nodesByLevel.end(), []( const auto& level ) { + std::for_each( level.begin(), level.end(), []( auto node ) { + if ( !node->m_initialized ) { node->init(); } + } ); + } ); + } +} + +bool DataflowGraph::execute() { + if ( !m_ready ) { + if ( !compile() ) { return false; } + } + bool result = true; + std::for_each( m_nodesByLevel.begin(), m_nodesByLevel.end(), [&result]( const auto& level ) { + std::for_each( level.begin(), level.end(), [&result]( auto node ) { + result = result && node->execute(); + } ); + } ); + return result; +} + +void DataflowGraph::destroy() { + std::for_each( + m_nodesByLevel.begin(), m_nodesByLevel.end(), []( auto& level ) { level.clear(); } ); + m_nodesByLevel.clear(); + m_nodes.clear(); + m_factories.reset(); + m_dataSetters.erase( m_dataSetters.begin(), m_dataSetters.end() ); + Node::destroy(); + m_ready = false; + m_shouldBeSaved = true; +} + +void DataflowGraph::saveToJson( const std::string& jsonFilePath ) { + if ( !jsonFilePath.empty() ) { + nlohmann::json data; + toJson( data ); + std::ofstream file( jsonFilePath ); + file << std::setw( 4 ) << data << std::endl; + m_shouldBeSaved = false; + } +} + +void DataflowGraph::toJsonInternal( nlohmann::json& data ) const { + nlohmann::json factories = nlohmann::json::array(); + nlohmann::json nodes = nlohmann::json::array(); + nlohmann::json connections = nlohmann::json::array(); + nlohmann::json model; + nlohmann::json graph; + + if ( m_factories ) { + for ( const auto& [name, factory] : *m_factories ) { + // do not save the standard factory, it will always be there + if ( name != NodeFactoriesManager::dataFlowBuiltInsFactoryName ) { + factories.push_back( name ); + } + } + graph["factories"] = factories; + } + + for ( const auto& n : m_nodes ) { + nlohmann::json nodeData; + n->toJson( nodeData ); + nodes.push_back( nodeData ); + for ( const auto& input : n->getInputs() ) { + if ( input->isLinked() ) { + nlohmann::json link = nlohmann::json::object(); + auto portOut = input->getLink(); + auto nodeOut = portOut->getNode(); + link["out_node"] = nodeOut->getInstanceName(); + link["out_port"] = portOut->getName(); + link["in_node"] = n->getInstanceName(); + link["in_port"] = input->getName(); + connections.push_back( link ); + } + } + } + + // write the common content of the Node to the json data + graph["nodes"] = nodes; + graph["connections"] = connections; + // Fill the specific concrete node information + data.emplace( "graph", graph ); +} + +bool DataflowGraph::loadFromJson( const std::string& jsonFilePath ) { + + if ( !nlohmann::json::accept( std::ifstream( jsonFilePath ) ) ) { + LOG( logERROR ) << jsonFilePath << " is not a valid json file !!"; + return false; + } + + std::ifstream file( jsonFilePath ); + nlohmann::json j; + file >> j; + m_shouldBeSaved = false; + return fromJson( j ); +} + +std::pair getLinkInfo( const std::string& which, + const nlohmann::json& linkData, + const std::map& nodeByName ) { + std::string field = which + "_node"; + Node* node { nullptr }; + + auto itNode = nodeByName.find( linkData[field] ); + if ( itNode != nodeByName.end() ) { node = itNode->second; } + else { + // Error, could not find the node + std::string msg = + std::string { "Node " } + which + " not found in cache " + " : " + linkData.dump(); + return { nullptr, msg }; + } + + std::string port; + field = which + "_port"; + if ( linkData.contains( field ) ) { + auto p = node->getPortByName( which, linkData[field] ); + if ( p != nullptr ) { port = p->getName(); } + } + else { + field = which + "_index"; + if ( linkData.contains( field ) ) { + auto p = node->getPortByIndex( which, linkData[field] ); + if ( p != nullptr ) { port = p->getName(); } + } + } + if ( port.empty() ) { + std::string msg = std::string { "Port " } + which + " not found in node " + + node->getInstanceName() + " : " + linkData.dump(); + return { nullptr, msg }; + } + return { node, port }; +} + +bool DataflowGraph::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "graph" ) ) { + // indicate that the graph must be recompiled after loading + m_ready = false; + // load the graph + m_factories.reset( new NodeFactorySet ); + addFactory( NodeFactoriesManager::getDataFlowBuiltInsFactory() ); + if ( data["graph"].contains( "factories" ) ) { + auto factories = data["graph"]["factories"]; + for ( const auto& factoryName : factories ) { + // Do not add factories already registered for the graph. + if ( m_factories->hasFactory( factoryName ) ) { continue; } + auto factory = NodeFactoriesManager::getFactory( factoryName ); + if ( factory ) { addFactory( factory ); } + else { + LOG( logERROR ) + << "DataflowGraph::loadFromJson : Unable to find a factory with name " + << factoryName; + return false; + } + } + } + std::map nodeByName; + auto nodes = data["graph"]["nodes"]; + for ( auto& n : nodes ) { + if ( !n["model"].contains( "name" ) ) { + LOG( logERROR ) << "Found a node without model description." << n.dump() + << "Unable to build an instance."; + return false; + } + std::string nodeTypeName = n["model"]["name"]; + std::string instanceName; + + if ( n.contains( "instance" ) ) { instanceName = n["instance"]; } + else { + LOG( logERROR ) << "Found a node of type " << nodeTypeName + << " without identification "; + return false; + } + auto newNode = m_factories->createNode( nodeTypeName, n, this ); + if ( newNode ) { + if ( !instanceName.empty() ) { + auto [it, inserted] = nodeByName.insert( { instanceName, newNode } ); + if ( !inserted ) { + LOG( logERROR ) << "DataflowGraph::loadFromJson : duplicated node name " + << nodeTypeName; + return false; + } + } + } + else { + LOG( logERROR ) << "Unable to create the node " << nodeTypeName; + return false; + } + } + auto links = data["graph"]["connections"]; + for ( auto& l : links ) { + auto [nodeFrom, fromOutput] = getLinkInfo( "out", l, nodeByName ); + if ( nodeFrom == nullptr ) { + LOG( logERROR ) << "DataflowGraph::loadFromJson: error when parsing JSON." + << " Could not find the link source (" << fromOutput + << "). Link not added."; + return false; + } + auto [nodeTo, toInput] = getLinkInfo( "in", l, nodeByName ); + if ( nodeTo == nullptr ) { + LOG( logERROR ) << "DataflowGraph::loadFromJson: error when parsing JSON." + << " Could not find the link source (" << toInput + << "). Link not added."; + return false; + } + if ( !addLink( nodeFrom, fromOutput, nodeTo, toInput ) ) { + LOG( logERROR ) + << "DataflowGraph::loadFromJson: error when parsing JSON" + << ": Could not add a link (missing or wrong information, please refer to " + "the previous error messages). Link not added."; + return false; + } + } + } + return true; +} + +bool DataflowGraph::canAdd( const Node* newNode ) const { + return findNode( newNode ) == -1; +} + +std::pair DataflowGraph::addNode( std::unique_ptr newNode ) { + // Check if the new node already exists (= same name and type) + if ( canAdd( newNode.get() ) ) { + if ( newNode->getInputs().empty() || newNode->getOutputs().empty() ) { + bool ( DataflowGraph::*addGraphIOPort )( PortBase* ) = newNode->getInputs().empty() + ? &DataflowGraph::addSetter + : &DataflowGraph::addGetter; + auto& interfaces = newNode->buildInterfaces( this ); + for ( auto p : interfaces ) { + ( this->*addGraphIOPort )( p ); + } + } + auto addedNode = newNode.get(); + m_nodes.emplace_back( std::move( newNode ) ); + m_ready = false; + m_shouldBeSaved = true; + return { true, addedNode }; + } + else { return { false, newNode.release() }; } +} + +bool DataflowGraph::removeNode( Node*& node ) { + // This is to prevent graph destruction from the graph editor, depending on how it is used + if ( m_nodesAndLinksProtected ) { return false; } + + // Check if the new node already exists (= same name) + int index = -1; + if ( ( index = findNode( node ) ) == -1 ) { return false; } + else { + if ( node->getInputs().empty() || node->getOutputs().empty() ) { + bool ( DataflowGraph::*removeGraphIOPort )( const std::string& ) = + node->getInputs().empty() ? &DataflowGraph::removeSetter + : &DataflowGraph::removeGetter; + for ( auto& p : node->getInterfaces() ) { + ( this->*removeGraphIOPort )( p->getName() ); + } + } + m_nodes.erase( m_nodes.begin() + index ); + node = nullptr; + m_ready = false; + m_shouldBeSaved = true; + return true; + } +} + +bool DataflowGraph::addLink( Node* nodeFrom, + const std::string& nodeFromOutputName, + Node* nodeTo, + const std::string& nodeToInputName ) { + // Check node "from" existence in the graph + if ( findNode( nodeFrom ) == -1 ) { + LOG( logERROR ) << "DataflowGraph::addLink Unable to find initial node " + << nodeFrom->getInstanceName(); + return false; + } + + // Check node "to" existence in the graph + if ( findNode( nodeTo ) == -1 ) { + LOG( logERROR ) << "DataflowGraph::addLink Unable to find destination node " + << nodeTo->getInstanceName(); + return false; + } + + // Check if node "from"'s output exists + int foundFrom = -1; + int index = 0; + for ( auto& output : nodeFrom->getOutputs() ) { + if ( output->getName() == nodeFromOutputName ) { + foundFrom = index; + break; + } + index++; + } + if ( foundFrom == -1 ) { + LOG( logERROR ) << "DataflowGraph::addLink Unable to find output port " + << nodeFromOutputName << " from initial node " + << nodeFrom->getInstanceName() << " (" << nodeFrom->getTypeName() << ")"; + return false; + } + + // Check if node "to"'s input exists + int foundTo = -1; + index = 0; + for ( auto& input : nodeTo->getInputs() ) { + if ( input->getName() == nodeToInputName ) { + foundTo = index; + break; + } + index++; + } + if ( foundTo == -1 ) { + LOG( logERROR ) << "DataflowGraph::addLink Unable to find input port " << nodeFromOutputName + << " from destination node " << nodeTo->getInstanceName() << " (" + << nodeTo->getTypeName() << ")"; + return false; + } + + // Compare types + if ( !( nodeTo->getInputs()[foundTo]->getType() == + nodeFrom->getOutputs()[foundFrom]->getType() ) ) { + LOG( logERROR ) << "DataflowGraph::addLink type mismatch from " + << nodeFrom->getInstanceName() << " (" << nodeFrom->getTypeName() << ") / " + << nodeFrom->getOutputs()[foundFrom]->getName() << " (" << foundFrom + << " with type " << nodeFrom->getOutputs()[foundFrom]->getTypeName() << ")" + << " to " << nodeTo->getInstanceName() << " (" << nodeTo->getTypeName() + << ") / " << nodeTo->getInputs()[foundTo]->getName() << " (" << foundTo + << " with type " << nodeTo->getInputs()[foundTo]->getTypeName() << ") "; + return false; + } + + // Check if input is connected + if ( nodeTo->getInputs()[foundTo]->isLinked() ) { + LOG( logERROR ) + << "DataflowGraph::addLink destination port not available (already linked) for " + << nodeTo->getInstanceName() << " (" << nodeFrom->getTypeName() << "), port " + << nodeTo->getInputs()[foundTo]->getName(); + return false; + } + + // port can be connected + nodeTo->getInputs()[foundTo]->connect( nodeFrom->getOutputs()[foundFrom].get() ); + // The state of the graph changes, set it to not ready + m_ready = false; + m_shouldBeSaved = true; + return true; +} + +bool DataflowGraph::removeLink( Node* node, const std::string& nodeInputName ) { + // This is to prevent graph destruction from the graph editor, depending on how it is used + if ( m_nodesAndLinksProtected ) { return false; } + + // Check node's existence in the graph + if ( findNode( node ) == -1 ) { return false; } + + // Check if node's input exists + int found = -1; + int index = 0; + for ( auto& input : node->getInputs() ) { + if ( input->getName() == nodeInputName ) { + found = index; + break; + } + index++; + } + if ( found == -1 ) { return false; } + + node->getInputs()[found]->disconnect(); + m_ready = false; + m_shouldBeSaved = true; + return true; +} + +int DataflowGraph::findNode( const Node* node ) const { + auto foundIt = std::find_if( + m_nodes.begin(), m_nodes.end(), [node]( const auto& p ) { return *p == *node; } ); + if ( foundIt != m_nodes.end() ) { return std::distance( m_nodes.begin(), foundIt ); } + else { return -1; } +} + +bool DataflowGraph::compile() { + // Find useful nodes (directly or indirectly connected to a Sink) + std::unordered_map>> infoNodes; + for ( auto const& n : m_nodes ) { + // Find all sinks + if ( n->getOutputs().size() == 0 ) { + // Add the sink in the useful nodes set if any of his port is linked + bool activeSink { false }; + for ( const auto& p : n->getInputs() ) { + activeSink |= p->isLinked(); + } + if ( activeSink ) { + infoNodes.emplace( n.get(), std::pair>( 0, {} ) ); + // recursively add the predecessors of the sink + backtrackGraph( n.get(), infoNodes ); + } + } + } + // Compute the level (rank of execution) of useful nodes + int maxLevel = 0; + for ( auto& infNode : infoNodes ) { + auto n = infNode.first; + // Compute the nodes' level starting from sources + if ( n->getInputs().empty() ) { + // Tag successors + for ( auto const successor : infNode.second.second ) { + infoNodes[successor].first = + std::max( infoNodes[successor].first, infoNodes[n].first + 1 ); + maxLevel = std::max( maxLevel, + std::max( infoNodes[successor].first, + goThroughGraph( successor, infoNodes ) ) ); + } + } + } + m_nodesByLevel.clear(); + m_nodesByLevel.resize( infoNodes.size() != 0 ? maxLevel + 1 : 0 ); + for ( auto& infNode : infoNodes ) { + m_nodesByLevel[infNode.second.first].push_back( infNode.first ); + } + + // For each level + for ( auto& lvl : m_nodesByLevel ) { + // For each node + for ( size_t j = 0; j < lvl.size(); j++ ) { + if ( !lvl[j]->compile() ) { return m_ready = false; } + // For each input + for ( size_t k = 0; k < lvl[j]->getInputs().size(); k++ ) { + if ( lvl[j]->getInputs()[k]->isLinkMandatory() && + !lvl[j]->getInputs()[k]->isLinked() ) { + return m_ready = false; + } + } + } + } + m_ready = true; + init(); + return m_ready; +} + +void DataflowGraph::clearNodes() { + for ( size_t i = 0; i < m_nodesByLevel.size(); i++ ) { + m_nodesByLevel[i].clear(); + m_nodesByLevel[i].shrink_to_fit(); + } + m_nodesByLevel.clear(); + m_nodesByLevel.shrink_to_fit(); + m_nodes.erase( m_nodes.begin(), m_nodes.end() ); + m_nodes.shrink_to_fit(); + m_inputs.erase( m_inputs.begin(), m_inputs.end() ); + m_inputs.shrink_to_fit(); + m_outputs.erase( m_outputs.begin(), m_outputs.end() ); + m_outputs.shrink_to_fit(); + m_dataSetters.erase( m_dataSetters.begin(), m_dataSetters.end() ); + m_shouldBeSaved = true; +} + +void DataflowGraph::backtrackGraph( + Node* current, + std::unordered_map>>& infoNodes ) { + for ( auto& input : current->getInputs() ) { + if ( input->getLink() ) { + Node* previous = input->getLink()->getNode(); + if ( previous ) { + auto previousInInfoNodes = infoNodes.find( previous ); + if ( previousInInfoNodes != infoNodes.end() ) { + // If the previous node is already in the map, + // find if the current node is already a successor node + auto& previousSuccessors = previousInInfoNodes->second.second; + bool foundCurrent = std::any_of( previousSuccessors.begin(), + previousSuccessors.end(), + [current]( auto c ) { return c == current; } ); + if ( !foundCurrent ) { + // If the current node is not a successor node, add it to the list + previousSuccessors.push_back( current ); + } + } + else { + // Add node to info nodes + std::vector successors; + successors.push_back( current ); + infoNodes.emplace( + previous, + std::pair>( 0, std::move( successors ) ) ); + backtrackGraph( previous, infoNodes ); + } + } + } + } +} + +int DataflowGraph::goThroughGraph( + Node* current, + std::unordered_map>>& infoNodes ) { + int maxLevel = 0; + if ( infoNodes.find( current ) != infoNodes.end() ) { + for ( auto const& successor : infoNodes[current].second ) { + infoNodes[successor].first = + std::max( infoNodes[successor].first, infoNodes[current].first + 1 ); + maxLevel = + std::max( infoNodes[successor].first, goThroughGraph( successor, infoNodes ) ); + } + } + return maxLevel; +} + +bool DataflowGraph::addSetter( PortBase* in ) { + bool found = false; + if ( m_dataSetters.find( in->getName() ) == m_dataSetters.end() ) { + addInput( in ); + auto portOut = std::shared_ptr( in->reflect( this, in->getName() ) ); + m_dataSetters.emplace( std::make_pair( + in->getName(), + DataSetter { DataSetterDesc { portOut, portOut->getName(), portOut->getTypeName() }, + in } ) ); + found = true; + } + return found; +} + +bool DataflowGraph::removeSetter( const std::string& setterName ) { + bool removed = false; + auto itS = m_dataSetters.find( setterName ); + if ( itS != m_dataSetters.end() ) { + auto& [desPort, in] = itS->second; + removeInput( in ); + m_dataSetters.erase( itS ); + removed = true; + } + return removed; +} + +bool DataflowGraph::addGetter( PortBase* out ) { + if ( out->is_input() ) { return false; } + // This is very similar to addOutput, except the data can't be set. + // Data pointer must be set by any sink at compile time, in the init function to refer to the + // data fetched from the associated input link + bool found = false; + // TODO check if this verification is needed ? + for ( auto& output : m_outputs ) { + if ( output->getName() == out->getName() ) { + found = true; + break; + } + } + if ( !found ) { m_outputs.emplace_back( out ); } + return !found; +} + +bool DataflowGraph::removeGetter( const std::string& getterName ) { + auto getterP = std::find_if( m_outputs.begin(), m_outputs.end(), [getterName]( const auto& p ) { + return p->getName() == getterName; + } ); + bool found = false; + if ( getterP != m_outputs.end() ) { + m_outputs.erase( getterP ); + found = true; + } + return found; +} + +bool DataflowGraph::releaseDataSetter( const std::string& portName ) { + auto setter = m_dataSetters.find( portName ); + bool found = false; + if ( setter != m_dataSetters.end() ) { + auto [desc, in] = setter->second; + in->disconnect(); + found = true; + } + return found; +} + +// Why is this method useful if it is the same than getDataSetter ? +bool DataflowGraph::activateDataSetter( const std::string& portName ) { + return getDataSetter( portName ) != nullptr; +} + +std::shared_ptr DataflowGraph::getDataSetter( const std::string& portName ) { + auto setter = m_dataSetters.find( portName ); + std::shared_ptr p { nullptr }; + if ( setter != m_dataSetters.end() ) { + auto [desc, in] = setter->second; + p = std::get<0>( desc ); + in->disconnect(); + p->connect( in ); + } + return p; +} + +std::vector DataflowGraph::getAllDataSetters() const { + std::vector r; + r.reserve( m_dataSetters.size() ); + for ( auto& s : m_dataSetters ) { + r.push_back( s.second.first ); + } + return r; +} + +PortBase* DataflowGraph::getDataGetter( const std::string& portName ) { + auto portIt = std::find_if( m_outputs.begin(), m_outputs.end(), [portName]( const auto& p ) { + return p->getName() == portName; + } ); + PortBase* g { nullptr }; + if ( portIt != m_outputs.end() ) { g = portIt->get(); } + return g; +} + +std::vector DataflowGraph::getAllDataGetters() const { + std::vector r; + r.reserve( m_outputs.size() ); + for ( auto& portOut : m_outputs ) { + r.emplace_back( portOut.get(), portOut->getName(), portOut->getTypeName() ); + } + return r; +} + +Node* DataflowGraph::getNode( const std::string& instanceNameNode ) const { + auto nodeIt = + std::find_if( m_nodes.begin(), m_nodes.end(), [instanceNameNode]( const auto& n ) { + return n->getInstanceName() == instanceNameNode; + } ); + if ( nodeIt != m_nodes.end() ) { return nodeIt->get(); } + LOG( logERROR ) << "DataflowGraph::getNode : The node with the instance name \"" + << instanceNameNode << "\" has not been found"; + return nullptr; +} + +DataflowGraph* DataflowGraph::loadGraphFromJsonFile( const std::string& filename ) { + if ( !nlohmann::json::accept( std::ifstream( filename ) ) ) { + LOG( logERROR ) << filename << " is not a valid json file !!"; + return nullptr; + } + + std::ifstream jsonFile( filename ); + nlohmann::json j; + jsonFile >> j; + + bool valid = false; + if ( j.contains( "instance" ) && j.contains( "model" ) ) { + valid = j["model"].contains( "name" ); + } + if ( !valid ) { + LOG( logERROR ) << "loadGraphFromJsonFile :" << filename + << " does not contain a valid json NodeGraph\n"; + return nullptr; + } + std::string instanceName = j["instance"]; + std::string graphType = j["model"]["name"]; + LOG( logINFO ) << "Loading the graph " << instanceName << ", with type " << graphType << "\n"; + + auto& fctMngr = Ra::Dataflow::Core::NodeFactoriesManager::getFactoryManager(); + auto ndldd = fctMngr.createNode( graphType, j ); + if ( ndldd == nullptr ) { + LOG( logERROR ) << "Unable to load a graph with type " << graphType << "\n"; + return nullptr; + } + + auto graph = dynamic_cast( ndldd ); + if ( graph != nullptr ) { + graph->m_shouldBeSaved = false; + return graph; + } + + LOG( logERROR ) << "Loaded graph not inheriting from DataflowGraph " << graphType << "\n"; + delete ndldd; + return nullptr; +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/DataflowGraph.hpp b/src/Dataflow/Core/DataflowGraph.hpp new file mode 100644 index 00000000000..4c99b7c5030 --- /dev/null +++ b/src/Dataflow/Core/DataflowGraph.hpp @@ -0,0 +1,328 @@ +#pragma once +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +/** + * \todo : clarify what is a source and how to use it + * if sources must be used through interface port only, delete the set_data on all sources + * \todo Are node aliases a should-have (to make editing more user friendly)??? + */ + +/** + * \brief Represent a set of connected nodes that define a Direct Acyclic Computational Graph + * Ownership of nodes is given to the graph at construction time. + * \todo make a "graph embedding node" that allow to seemlesly integrate a graph as a node in + * another graph + * --> Edition of a graph will allow loading and saving to a file directly + * --> Edition of an embeded graph will defer loading and saving to the parent graph + * --> for this, need to decide if a subgraph is stored in the json of its parent or in a + * separate file + */ +class RA_DATAFLOW_API DataflowGraph : public Node +{ + public: + /// Constructor. + /// The nodes pointing to external data are created here. + /// \param name The name of the render graph. + explicit DataflowGraph( const std::string& name ); + virtual ~DataflowGraph() = default; + + void init() override; + bool execute() override; + void destroy() override; + + /// \brief Set the factories to use when loading a graph. + /// \param factories new factory set that will replace the existing factory set if any. + void setNodeFactories( std::shared_ptr factories ); + + /// \brief Get the node factories associated with the graph. + /// \return returns nullptr if no factory set is associated with the graph. + std::shared_ptr getNodeFactories() const; + + /// \brief Add a factory to the factory set of the graph. + /// Creates the factory set if it does not exists + /// \param f a shared pointer to the factory to be added. The name of this factory + void addFactory( std::shared_ptr f ); + + /// \brief Remove a factory from the factory set of the graph. + /// \param name the name of the factory to remove + /// \return true if the factory was found and removed + bool removeFactory( const std::string& name ); + + /// \brief Loads nodes and links from a JSON file. + /// \param jsonFilePath The path to the JSON file. + /// \return true if the file was loaded, false if an error occurs. + bool loadFromJson( const std::string& jsonFilePath ); + + /// \brief Saves nodes and links to a JSON file. + /// \param jsonFilePath The path to the JSON file. + void saveToJson( const std::string& jsonFilePath ); + + /// \brief Adds a node to the render graph. + /// Adds interface ports to the node newNode and the corresponding input and output ports to + /// the graph. + /// \param newNode The node to add to the graph. + /// \return a pair with a bool and a raw pointer to the Node. If the bool is true, the raw + /// pointer is owned by the graph. If the bool is false, the raw pointer ownership is left to + /// the caller. + virtual std::pair addNode( std::unique_ptr newNode ); + + /// \brief Removes a node from the render graph. + /// Removes input and output ports, corresponding to interface ports of the node, from the + /// graph. \param node The node to remove from the graph. \return true if the node was removed + /// and the given pointer is set to nullptr, false else + virtual bool removeNode( Node*& node ); + + /// Connects two nodes of the render graph. + /// The two nodes must already be in the render graph (with the addNode(Node* newNode) + /// function), the first node's in port must be free and the connected in port and out port must + /// have the same type of data. + /// \param nodeFrom The node that contains the out port. + /// \param nodeFromOutputName The name of the out port in nodeFrom. + /// \param nodeTo The node that contains the in port. + /// \param nodeToInputName The name of the in port in nodeTo. + bool addLink( Node* nodeFrom, + const std::string& nodeFromOutputName, + Node* nodeTo, + const std::string& nodeToInputName ); + + /// + /// \brief Removes the link connected to a node's input port + /// \param node the node to unlink + /// \param nodeInputName the name of the port to unlink + /// \return true if link is removed, false if not. + bool removeLink( Node* node, const std::string& nodeInputName ); + + /// \brief Get the vector of all the nodes on the graph + /// \return + const std::vector>& getNodes() const; + + /// Gets a specific node according to its instance name. + /// \param instanceNameNode The instance name of the node. + Node* getNode( const std::string& instanceNameNode ) const; + + /// Gets the nodes ordered by level (after compilation) + const std::vector>& getNodesByLevel() const; + + /// Compile the render graph to check its validity and simplify it. + /// The compilation has multiple goals: + /// - Remove the nodes that have no direct or indirect connections to sink nodes + /// - Order the nodes by level according to their dependencies + /// - Check if every mandatory port is linked + bool compile() override; + + /// Gets the number of nodes + size_t getNodesCount() const; + + /// Deletes all nodes from the render graph. + virtual void clearNodes(); + + /// Test if the graph is compiled + bool isCompiled() const; + + /// Mark the graph as needing recompilation (useful to force recompilation and resources update) + void needsRecompile(); + + /// Flag that indicates if the graph should be saved to a file + /// This flag is useless outside an load/edit/save scenario + bool m_shouldBeSaved { false }; + + /// \brief Gets an output port connected to the named input port of the graph. + /// Return the connected output port if success, sharing the ownership with the caller. + /// This output port could then be used through setter->setData( ptr ) to set the graph input + /// from the data pointer owned by the caller. + /// \note As ownership is shared with the caller, the graph must survive the returned + /// pointer to be able to use the dataSetter.. + /// \params portName The name of the input port of the graph + std::shared_ptr getDataSetter( const std::string& portName ); + + /// \brief disconnect the data setting port from its inputs. + bool releaseDataSetter( const std::string& portName ); + /// \brief connect the data setting port from its inputs. + bool activateDataSetter( const std::string& portName ); + + /// \brief Returns an alias to the named output port of the graph. + /// Allows to get the data stored at this port after the execution of the graph. + /// \note ownership is left to the graph, not shared. The graph must survive the returned + /// pointer to be able to use the dataGetter. + /// \params portName the name of the output port + PortBase* getDataGetter( const std::string& portName ); + + /// \brief Data setter descriptor. + /// A Data setter descriptor is composed of an output port (linked by construction to an input + /// port of the graph), its name and its type. + /// Use setData on the output port to pass data to the graph + using DataSetterDesc = std::tuple, std::string, std::string>; + + /// \brief Data getter descriptor. + /// A Data getter descriptor is composed of an output port (belonging to any node of the graph), + /// its name and its type. + /// Use getData on the output port to extract data from the graph. + /// \note, a dataGetter is valid only after successful compilation of the graph. + /// \todo find a way to test the validity of the getter (invalid if no path exists from any + /// source port to the associated sink port) + using DataGetterDesc = std::tuple; + + /// Creates a vector that stores all the existing DataSetters (\see getDataSetter) of the graph. + /// TODO : Verify why, when listing the data setters, they are connected ... + std::vector getAllDataSetters() const; + + /// Creates a vector that stores all the existing DataGetters (\see getDataGetter) of the graph. + /// A tuple is composed of an output port belonging to the graph, its name its type. + std::vector getAllDataGetters() const; + + protected: + /** Allow derived class to construct the graph with their own static type + */ + DataflowGraph( const std::string& instanceName, const std::string& typeName ); + + bool fromJsonInternal( const nlohmann::json& data ) override; + void toJsonInternal( nlohmann::json& ) const override; + + /// \brief Test if a node can be added to a graph + /// \param newNode const naked pointer to the candidate node + /// \return true if ownership could be transferred to the graph. + virtual bool canAdd( const Node* newNode ) const; + + private: + /// Flag set after successful compilation indicating graph is ready to be executed + /// This flag is reset as soon as the graph is modified. + bool m_ready { false }; + + /// The node factory to use for loading + std::shared_ptr m_factories; + /// The unordered list of nodes. + std::vector> m_nodes; + // Internal node levels representation + /// The list of nodes ordered by levels. + /// Two nodes at the same level have no dependency between them. + std::vector> m_nodesByLevel; + + // Internal helper functions + /// Internal compilation function that allows to go back in the render graph while filling an + /// information map. + /// \param current The current node. + /// \param infoNodes The map that contains information about nodes. + void backtrackGraph( Node* current, + std::unordered_map>>& infoNodes ); + /// Internal compilation function that allows to go through the render graph, using an + /// information map. + /// \param current The current node. + /// \param infoNodes The map that contains information about nodes. + int goThroughGraph( Node* current, + std::unordered_map>>& infoNodes ); + /// Returns the index of the given node in the graph. + /// if there is none, returns -1. + /// \param name The name of the node to find. + int findNode( const Node* node ) const; + + /// Data setters management : used to pass parameter to the graph when the graph is not embedded + /// into another graph (inputs are here for this case). + /// A dataSetter is an outputPort, associated to an input port of the graph. + /// The connection between these ports can be activated/deactivated using + /// activateDataSetter/releaseDataSetter + using DataSetter = std::pair; + std::map m_dataSetters; + + /// \brief Adds an input port to the graph and associate it with a dataSetter. + /// This port is aliased as an interface port in a source node of the graph. + /// This function checks if there is no input port with the same name already + /// associated with the graph. + bool addSetter( PortBase* in ); + + /// \brief Remove the given setter from the graph + /// \param setterName + /// \return true if the setter was removed, false else. + bool removeSetter( const std::string& setterName ); + + /// \brief Adds an out port for a Graph and register it as a dataGetter. + /// This port is aliased as an interface port in a sink node of the graph. + /// This function checks if there is no out port with the same name already + /// associated with the graph. + /// \param out The port to add. + bool addGetter( PortBase* out ); + + /// \brief Remove the given getter from the graph + /// \param getterName + /// \return true if the getter was removed, false else. + bool removeGetter( const std::string& getterName ); + + public: + static const std::string& getTypename(); + + /** + * \brief Load a graph from the given file. + * \param filename + * Any type of graph that inherits from DataflowGraph can be loaded by this function as soon as + * the appropriate constructor is registered in the node factory. + * \return The loaded graph, as a DataFlowGraph pointer to be downcast to the correct type + */ + static DataflowGraph* loadGraphFromJsonFile( const std::string& filename ); + + /** + * \brief protect nodes and links from deletion. + * \param on true to protect, false to unprotect. + */ + void setNodesAndLinksProtection( bool on ) { m_nodesAndLinksProtected = on; } + + /** + * \brief get the protection status protect nodes and links from deletion + * \return the protection status + */ + bool getNodesAndLinksProtection() const { return m_nodesAndLinksProtected; } + + private: + bool m_nodesAndLinksProtected { false }; +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- +inline bool DataflowGraph::isCompiled() const { + return m_ready; +} + +inline void DataflowGraph::needsRecompile() { + m_ready = false; +} + +inline void DataflowGraph::setNodeFactories( std::shared_ptr factories ) { + m_factories = factories; +} + +inline std::shared_ptr DataflowGraph::getNodeFactories() const { + return m_factories; +} + +inline void DataflowGraph::addFactory( std::shared_ptr f ) { + if ( !m_factories ) { m_factories.reset( new NodeFactorySet ); } + m_factories->addFactory( f ); +} + +inline bool DataflowGraph::removeFactory( const std::string& name ) { + return m_factories->removeFactory( name ); +} + +inline const std::vector>& DataflowGraph::getNodes() const { + return m_nodes; +} +inline const std::vector>& DataflowGraph::getNodesByLevel() const { + return m_nodesByLevel; +} +inline size_t DataflowGraph::getNodesCount() const { + return m_nodes.size(); +} +inline const std::string& DataflowGraph::getTypename() { + static std::string demangledTypeName { "Core DataflowGraph" }; + return demangledTypeName; +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/EditableParameter.hpp b/src/Dataflow/Core/EditableParameter.hpp new file mode 100644 index 00000000000..ea6b5ac1737 --- /dev/null +++ b/src/Dataflow/Core/EditableParameter.hpp @@ -0,0 +1,97 @@ +#pragma once +#include + +#include +#include +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +/// \brief Basic introspection for Node internal data edition +/// This base class gives key information to associate editing capabilities (and gui) to +/// an node internal data. +/// \note This class seems to be very similar in its aim than the Ra::Engine::RenderParameter and +/// their editing capabilities through Ra::Gui::ParameterSetEditor. But, in order to be more +/// general, this class does not depend on Engine. +/// \todo Unify with Ra::Engine::RenderParameter (using Core only parameters set) +/// +struct RA_DATAFLOW_API EditableParameterBase { + /// \name Constructors + /// @{ + /// \brief delete default constructors. + EditableParameterBase() = delete; + EditableParameterBase( const EditableParameterBase& ) = delete; + EditableParameterBase& operator=( const EditableParameterBase& ) = delete; + + /// Construct an base editable parameter from its name and type hash + EditableParameterBase( const std::string& name, std::type_index typeIdx ); + ///@} + + virtual ~EditableParameterBase() = default; + std::string getName() const; + std::type_index getType() const; + /// Constraints on the edited data : json object describing the constraints + /// Add constraints or associated data to the editable. + void setConstraints( const nlohmann::json& constraints ); + /// get the constraints or the associated data from the editable. + const nlohmann::json& getConstraints() const; + + private: + std::string m_name { "" }; + std::type_index m_typeIdx; + + /// Constraints on the edited data + nlohmann::json m_constraints; +}; + +template +struct EditableParameter : public EditableParameterBase { + /// \name Constructors + /// @{ + /// \brief delete default constructors. + EditableParameter() = delete; + EditableParameter( const EditableParameter& ) = delete; + EditableParameter& operator=( const EditableParameter& ) = delete; + + /// Construct an editable parameter from its name and type hash + EditableParameter( const std::string& name, T& data ); + ///@} + + /// The data to edit. + /// This is a reference to any data stored in a node and that the user could change + T& m_data; +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +inline EditableParameterBase::EditableParameterBase( const std::string& name, + std::type_index typeIdx ) : + m_name( name ), m_typeIdx( typeIdx ) {} + +template +EditableParameter::EditableParameter( const std::string& name, T& data ) : + EditableParameterBase( name, typeid( T ) ), m_data( data ) {} + +inline std::string EditableParameterBase::getName() const { + return m_name; +} + +inline std::type_index EditableParameterBase::getType() const { + return m_typeIdx; +} + +inline void EditableParameterBase::setConstraints( const nlohmann::json& constraints ) { + m_constraints = constraints; +} + +inline const nlohmann::json& EditableParameterBase::getConstraints() const { + return m_constraints; +} +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Enumerator.hpp b/src/Dataflow/Core/Enumerator.hpp new file mode 100644 index 00000000000..f8e1d8446a1 --- /dev/null +++ b/src/Dataflow/Core/Enumerator.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +/** + * \brief This class might be removed in a future and replaced by an instantiation of + * Ra::Core::Utils::BijectiveAssociation. + * + * This class allows to associate Values of type T to an int. + * Used right now to build the Node edition UI. + * \tparam T + */ +template +class Enumerator : public Ra::Core::Utils::Observable&> +{ + std::vector m_values; + size_t m_currentIndex { 0 }; + T* m_currentValue; + + public: + explicit Enumerator( std::initializer_list values ); + const T& get() const; + size_t size() const; + bool set( size_t p ); + bool set( const T& v ); + typename std::vector::const_iterator begin() const; + typename std::vector::const_iterator end() const; + const T& operator[]( size_t p ) const; +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +Enumerator::Enumerator( std::initializer_list values ) : + m_values { values }, m_currentValue { m_values.data() } {} + +template +const T& Enumerator::get() const { + return *m_currentValue; +} + +template +size_t Enumerator::size() const { + return m_values.size(); +} + +template +bool Enumerator::set( size_t p ) { + if ( p < m_values.size() ) { + m_currentValue = m_values.data() + p; + m_currentIndex = p; + this->notify( *this ); + return true; + } + else { return false; } +} + +template +bool Enumerator::set( const T& v ) { + size_t p = 0; + for ( const auto& e : m_values ) { + if ( e == v ) { return set( p ); } + p++; + } + return false; +} + +template +typename std::vector::const_iterator Enumerator::begin() const { + return m_values.cbegin(); +} + +template +typename std::vector::const_iterator Enumerator::end() const { + return m_values.cend(); +} + +template +const T& Enumerator::operator[]( size_t p ) const { + return m_values.at( p ); +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Node.cpp b/src/Dataflow/Core/Node.cpp new file mode 100644 index 00000000000..4d741b1f453 --- /dev/null +++ b/src/Dataflow/Core/Node.cpp @@ -0,0 +1,84 @@ +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +using namespace Ra::Core::Utils; + +Node::Node( const std::string& instanceName, const std::string& typeName ) : + m_typeName { typeName }, m_instanceName { instanceName } {} + +bool Node::fromJson( const nlohmann::json& data ) { + if ( data.empty() ) { + // This is to avoid wrong error message when creating node from the editor + return true; + } + + if ( data.contains( "instance" ) ) { m_instanceName = data["instance"]; } + else { + LOG( logERROR ) << "Missing required instance name when loading node " << m_instanceName; + return false; + } + // get the common content of the Node from the json data + bool loaded = false; + if ( data.contains( "model" ) ) { + // get the specific concrete node information + const auto& datamodel = data["model"]; + loaded = fromJsonInternal( datamodel ); + } + else { + LOG( logERROR ) << "Missing required model when loading a Dataflow::Node"; + loaded = false; + } + // get the supplemental information related to application/gui/... + for ( auto& [key, value] : data.items() ) { + if ( key != "instance" && key != "model" ) { m_extraJsonData.emplace( key, value ); } + } + return loaded; +} + +void Node::toJson( nlohmann::json& data ) const { + + // write the common content of the Node to the json data + data["instance"] = m_instanceName; + + nlohmann::json model; + model["name"] = m_typeName; + + // Fill the specific concrete node information (model instance) + toJsonInternal( model ); + data.emplace( "model", model ); + + // store the supplemental information related to application/gui/... + for ( auto& [key, value] : m_extraJsonData.items() ) { + if ( key != "instance" && key != "model" ) { data.emplace( key, value ); } + } +} + +void Node::addJsonMetaData( const nlohmann::json& data ) { + for ( auto& [key, value] : data.items() ) { + m_extraJsonData[key] = value; + } +} + +PortBase* Node::getPortByName( const std::string& type, const std::string& name ) const { + const auto& ports = ( type == "in" ) ? m_inputs : m_outputs; + auto itp = std::find_if( + ports.begin(), ports.end(), [n = name]( const auto& p ) { return p->getName() == n; } ); + PortBase* fprt { nullptr }; + if ( itp != ports.cend() ) { fprt = itp->get(); } + return fprt; +} + +PortBase* Node::getPortByIndex( const std::string& type, int idx ) const { + const auto& ports = ( type == "in" ) ? m_inputs : m_outputs; + if ( 0 <= idx && size_t( idx ) < ports.size() ) { return ports[idx].get(); } + return nullptr; +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Node.hpp b/src/Dataflow/Core/Node.hpp new file mode 100644 index 00000000000..ecd2e3a014f --- /dev/null +++ b/src/Dataflow/Core/Node.hpp @@ -0,0 +1,393 @@ +#pragma once +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +/** \brief Base abstract class for all the nodes added and used by the node system. + * A node represent a function acting on some input data and generating some outputs. + * To build a computation graph, nodes should be added to the graph, which is itself a node + * (\see Ra::Dataflow::Core::DataflowGraph) and linked together through their input/output port. + * + * Nodes computes their function using the input data collecting from the input ports, + * in an evaluation context (possibly empty) defined byt their internal data to generate results + * sent to their output ports. + * + */ +class RA_DATAFLOW_API Node +{ + public: + /// \name Constructors + /// @{ + /// \brief delete default constructors. + /// \see https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-copy-virtual + Node() = delete; + Node( const Node& ) = delete; + Node& operator=( const Node& ) = delete; + /// @} + + /// \brief make Node a base abstract class + virtual ~Node() = default; + + /// \brief Two nodes are considered equal if there type and instance names are the same. + bool operator==( const Node& o_node ); + + /// \name Function execution control + /// @{ + /// \brief Initializes the node content + /// The init() function should be called once at the beginning of the lifetime of the node by + /// the owner of the node (the graph which contains the node). + /// Its goal is to initialize the node's internal data if any. + /// The base version do nothing. + virtual void init(); + + /// \brief Compile the node to check its validity + /// Only nodes defining a full computation graph will need to override this method. + /// The base version do nothing. + /// \return the compilation status + virtual bool compile(); + + /// \brief Executes the node. + /// Execute the node function on the input ports (to be fetched) and write the results to the + /// output ports. + /// \return the execution status. + virtual bool execute() = 0; + + /// \brief delete the node content + /// The destroy() function is called once at the end of the lifetime of the node. + /// Its goal is to free the internal data that have been allocated. + virtual void destroy(); + /// @} + + /// \name Control the interfaces of the nodes (inputs, outputs, internal data, ...) + /// @{ + + /// \brief Get an input port by its name + /// \param type either "in" or "out", the directional type of the port + /// \param name + /// \return an alias pointer on the requested port if it exists, nullptr else + PortBase* getPortByName( const std::string& type, const std::string& name ) const; + + /// \brief Get an input port by its index + /// \param type either "in" or "out", the directional type of the port + /// \param idx + /// \return an alias pointer on the requested port if it exists, nullptr else + PortBase* getPortByIndex( const std::string& type, int idx ) const; + + /// \brief Gets the in ports of the node. + /// Input ports are own to the node. + const std::vector>& getInputs() const; + + /// \brief Gets the out ports of the node. + /// Output ports are own to the node. + const std::vector>& getOutputs() const; + + /// \brief Build the interface ports of the node + /// Derived node can override the default implementation that build an interface port for each + /// input or output port (e.g. if several inputs have the same type T, make an interface that is + /// a vector of T*) + virtual const std::vector& buildInterfaces( Node* parent ); + + /// \brief Get the interface ports of the node + const std::vector& getInterfaces() const; + + /// \brief Gets the editable parameters of the node. + /// used only by the node editor gui to build the editon widget + const std::vector>& getEditableParameters(); + /// @} + + /// \name Identification methods + /// @{ + /// \brief Gets the type name of the node. + const std::string& getTypeName() const; + + /// \brief Gets the instance name of the node. + const std::string& getInstanceName() const; + + /// \brief Sets the instance name (rename) the node + void setInstanceName( const std::string& newName ); + + /// @} + + /// \name Serialization of a node + /// @{ + /// TODO : specify the json format for nodes and what is expected from the following ethods + + /// \brief serialize the content of the node. + /// Fill the given json object with the json representation of the concrete node. + void toJson( nlohmann::json& data ) const; + + /// \brief unserialized the content of the node. + /// Fill the node from its json representation + bool fromJson( const nlohmann::json& data ); + + /// \brief Add a metadata to the node to store application specific information. + /// used, e.g. by the node editor gui to save node position in the graphical canvas. + void addJsonMetaData( const nlohmann::json& data ); + + /// \brief Give access to extra json data stored on the node. + const nlohmann::json& getJsonMetaData(); + /// @} + + /// \brief Flag that checks if the node is already initialized + bool m_initialized { false }; + + /// \brief Sets the filesystem (real or virtual) location for the node resources + inline void setResourcesDir( std::string resourcesRootDir ); + + protected: + /// Construct the base node given its name and type + /// \param instanceName The name of the node + /// \param typeName The type name of the node + Node( const std::string& instanceName, const std::string& typeName ); + + /// internal json representation of the Node. + /// Must be implemented by inheriting classes. + /// Be careful with template specialization and function member overriding when implementing + /// this method. + virtual bool fromJsonInternal( const nlohmann::json& ) = 0; + + /// internal json representation of the Node. + /// Must be implemented by inheriting classes. + /// Be careful with template specialization and function member overriding when implementing + /// this method. + virtual void toJsonInternal( nlohmann::json& ) const = 0; + + /// Adds an in port to the node. + /// This function checks if the port is an input port, then if there is no in port with the same + /// name already associated with this node. + /// \param in The in port to add. + bool addInput( PortBase* in ); + + /// \brief remove the given input port from the managed input ports + /// \param in the port to remove + /// \return true if the port was removed (the in pointer is the set to nullptr), false else + bool removeInput( PortBase*& in ); + + /// Adds an out port to the node and the data associated with it. + /// This function checks if there is no out port with the same name already associated with this + /// node. + /// \param out The in port to add. + /// \param data The data associated with the port. + template + void addOutput( PortOut* out, T* data ); + + /// \brief remove the given output port from the managed input ports + /// \param out the port to remove + /// \return true if the port was removed (the out pointer is the set to nullptr), false else + bool removeOutput( PortBase*& out ); + + /// \brief Adds an editable parameter to the node if it does not already exist. + /// \note the node will take ownership of the editable object. + /// \param editableParameter The editable parameter to add. + bool addEditableParameter( EditableParameterBase* editableParameter ); + + /// Remove an editable parameter to the node if it does exist. + /// \param name The name of the editable parameter to remove. + /// \return true if the editable parameter is found and removed. + bool removeEditableParameter( const std::string& name ); + + /// \brief get a typed reference to the editable parameter. + /// \tparam E The type of the expected editable parameter. + /// \param name The name of the editable parameter to get. + /// \return the pointer to the editable parameter if any, nullptr if not. + template + EditableParameter* getEditableParameter( const std::string& name ); + + /// The type name of the node. Initialized once at construction + std::string m_typeName; + /// The instance name of the node + std::string m_instanceName; + /// The in ports of the node (own by the node) + std::vector> m_inputs; + /// The out ports of the node (own by the node) + std::vector> m_outputs; + /// The reflected ports of the node if it is only a source or sink node. + /// This stores only aliases as interface ports will belong to the parent + /// node (i.e. the graph this node belongs to) + std::vector m_interface; + + /// The editable parameters of the node + /// \todo replace this by a Ra::Core::VariableSet + std::vector> m_editableParameters; + + /// Additional data on the node, added by application or gui or ... + nlohmann::json m_extraJsonData; + + public: + /// \brief Returns the demangled type name of the node or any human readable representation of + /// the type name. + /// This is a public static member each node must define to be serializable + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +inline void Node::init() { + m_initialized = true; +} + +inline void Node::destroy() { + m_interface.clear(); + m_inputs.clear(); + m_outputs.clear(); + m_editableParameters.clear(); +} + +inline const nlohmann::json& Node::getJsonMetaData() { + return m_extraJsonData; +} + +inline const std::string& Node::getTypeName() const { + return m_typeName; +} + +inline const std::string& Node::getInstanceName() const { + return m_instanceName; +} + +inline void Node::setInstanceName( const std::string& newName ) { + m_instanceName = newName; +} + +inline const std::vector>& Node::getInputs() const { + return m_inputs; +} + +inline const std::vector>& Node::getOutputs() const { + return m_outputs; +} + +inline const std::vector& Node::buildInterfaces( Node* parent ) { + m_interface.clear(); + m_interface.shrink_to_fit(); + const std::vector>* readFrom = + m_inputs.empty() ? &m_outputs : &m_inputs; + m_interface.reserve( readFrom->size() ); + for ( const auto& p : *readFrom ) { + m_interface.emplace_back( p->reflect( parent, getInstanceName() + '_' + p->getName() ) ); + } + return m_interface; +} + +inline const std::vector& Node::getInterfaces() const { + return m_interface; +} + +inline const std::vector>& Node::getEditableParameters() { + return m_editableParameters; +} + +inline bool Node::operator==( const Node& o_node ) { + return ( m_typeName == o_node.getTypeName() ) && ( m_instanceName == o_node.getInstanceName() ); +} + +inline bool Node::addInput( PortBase* in ) { + if ( !in->is_input() ) { return false; } + bool found = false; + for ( auto& input : m_inputs ) { + if ( input->getName() == in->getName() ) { found = true; } + } + if ( !found ) { m_inputs.emplace_back( in ); } + return !found; +} + +inline bool Node::removeInput( PortBase*& in ) { + auto itP = std::find_if( + m_inputs.begin(), m_inputs.end(), [in]( const auto& p ) { return p.get() == in; } ); + if ( itP != m_inputs.end() ) { + m_inputs.erase( itP ); + in = nullptr; + return true; + } + return false; +} + +template +void Node::addOutput( PortOut* out, T* data ) { + bool found = false; + for ( auto& output : m_outputs ) { + if ( output->getName() == out->getName() ) { found = true; } + } + if ( !found ) { + m_outputs.emplace_back( out ); + out->setData( data ); + } +} + +inline bool Node::removeOutput( PortBase*& out ) { + auto outP = std::find_if( + m_outputs.begin(), m_outputs.end(), [out]( const auto& p ) { return p.get() == out; } ); + if ( outP != m_outputs.end() ) { + m_outputs.erase( outP ); + out = nullptr; + return true; + } + return false; +} + +inline bool Node::addEditableParameter( EditableParameterBase* editableParameter ) { + auto edIt = std::find_if( + m_editableParameters.begin(), + m_editableParameters.end(), + [name = editableParameter->getName()]( const auto& p ) { return p->getName() == name; } ); + if ( edIt == m_editableParameters.end() ) { + m_editableParameters.emplace_back( editableParameter ); + } + return edIt == m_editableParameters.end(); +} + +inline bool Node::removeEditableParameter( const std::string& name ) { + bool found = false; + auto it = m_editableParameters.begin(); + while ( it != m_editableParameters.end() ) { + if ( ( *it ).get()->getName() == name ) { + m_editableParameters.erase( it ); + found = true; + break; + } + ++it; + } + return found; +} + +template +inline EditableParameter* Node::getEditableParameter( const std::string& name ) { + auto it = m_editableParameters.begin(); + while ( it != m_editableParameters.end() ) { + if ( ( *it ).get()->getName() == name ) { + auto p = dynamic_cast*>( ( *it ).get() ); + if ( p != nullptr ) { return *p; } + } + ++it; + } + return nullptr; +} + +inline const std::string& Node::getTypename() { + static std::string demangledTypeName { "Abstract Node" }; + return demangledTypeName; +} + +inline bool Node::compile() { + return true; +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/NodeFactory.cpp b/src/Dataflow/Core/NodeFactory.cpp new file mode 100644 index 00000000000..dbe230b7bf7 --- /dev/null +++ b/src/Dataflow/Core/NodeFactory.cpp @@ -0,0 +1,106 @@ +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace NodeFactoriesManager { +const std::string dataFlowBuiltInsFactoryName { "DataFlowBuiltIns" }; +} + +NodeFactory::NodeFactory( std::string name ) : m_name( std::move( name ) ) {} + +auto NodeFactory::getName() const -> std::string { + return m_name; +} + +auto NodeFactory::createNode( const std::string& nodeType, + const nlohmann::json& data, + DataflowGraph* owningGraph ) -> Node* { + if ( auto itr = m_nodesCreators.find( nodeType ); itr != m_nodesCreators.end() ) { + auto* node = itr->second.first( data ); + if ( owningGraph != nullptr ) { + auto [added, n] = owningGraph->addNode( std::unique_ptr( node ) ); + return n; + } + return node; + } + return nullptr; +} + +auto NodeFactory::registerNodeCreator( const std::string& nodeType, + NodeCreatorFunctor nodeCreator, + const std::string& nodeCategory ) -> bool { + + if ( auto itr = m_nodesCreators.find( nodeType ); itr == m_nodesCreators.end() ) { + m_nodesCreators[nodeType] = { std::move( nodeCreator ), nodeCategory }; + return true; + } + LOG( Ra::Core::Utils::logWARNING ) + << "NodeFactory (" << getName() + << ") : trying to add an already existing node creator for type " << nodeType << "."; + return false; +} + +auto NodeFactory::nextNodeId() -> size_t { + return ++m_nodesCreated; +} + +auto NodeFactorySet::createNode( const std::string& nodeType, + const nlohmann::json& data, + DataflowGraph* owningGraph ) -> Node* { + for ( const auto& itr : m_factories ) { + if ( auto* node = itr.second->createNode( nodeType, data, owningGraph ); node ) { + return node; + } + } + LOG( Ra::Core::Utils::logERROR ) << "NodeFactorySet: unable to find constructor for " + << nodeType << " in any managed factory."; + return nullptr; +} + +namespace NodeFactoriesManager { + +/** + * \brief Allow static initialization without init order problems + * \return The manager singleton + */ +auto getFactoryManager() -> NodeFactorySet& { + static NodeFactorySet s_factoryManager {}; + return s_factoryManager; +} + +auto registerFactory( NodeFactorySet::mapped_type factory ) -> bool { + return getFactoryManager().addFactory( std::move( factory ) ); +} + +auto createFactory( const NodeFactorySet::key_type& name ) -> NodeFactorySet::mapped_type { + auto factory = getFactory( name ); + if ( factory == nullptr ) { + factory = std::make_shared( name ); + registerFactory( factory ); + } + return factory; +} + +auto getFactory( const NodeFactorySet::key_type& name ) -> NodeFactorySet::mapped_type { + auto& fctMgr = getFactoryManager(); + if ( auto factory = fctMgr.find( name ); factory != fctMgr.end() ) { return factory->second; } + return nullptr; +} + +auto unregisterFactory( const NodeFactorySet::key_type& name ) -> bool { + return getFactoryManager().removeFactory( name ); +} + +auto getDataFlowBuiltInsFactory() -> NodeFactorySet::mapped_type { + return getFactory( NodeFactoriesManager::dataFlowBuiltInsFactoryName ); +} + +} // namespace NodeFactoriesManager + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/NodeFactory.hpp b/src/Dataflow/Core/NodeFactory.hpp new file mode 100644 index 00000000000..82897987ce6 --- /dev/null +++ b/src/Dataflow/Core/NodeFactory.hpp @@ -0,0 +1,321 @@ +#pragma once +#include + +#include + +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +class DataflowGraph; + +/** + * NodeFactory store a set of functions allowing to dynamically create dataflow nodes. + * A NodeFactory is used when loading a node graph from a json representation of the graph to + * instantiate all the loaded nodes. + * Each DataflowGraph must have a reference to the nodeFactory to be used to create all the node he + * has. + * + */ +class RA_DATAFLOW_API NodeFactory +{ + public: + /** Creates an empty factory with the given name */ + explicit NodeFactory( std::string name ); + + [[nodiscard]] auto getName() const -> std::string; + + /** Function that creates and initialize a node. + * Typical implementation of such a function should do the following : + * { + * auto node = new ConcreteNodeType( unique_instance_name ); + * node->fromJson( data); + * return node; + * } + */ + using NodeCreatorFunctor = std::function; + + /** + * Associate, for a given concrete node type, a custom NodeCreatorFunctor + * @tparam T Concrete node type identifier + * \param nodeCreator Functor to create an node of the corresponding concrete node type. + * \param nodeCategory Category of the node. + * \return true if the node creator is successfully added, false if not (e.g. due to a name + * collision). + */ + template + auto registerNodeCreator( NodeCreatorFunctor nodeCreator, + const std::string& nodeCategory = "RadiumNodes" ) -> bool; + + /** + * Associate, for a given concrete node type, a generic NodeCreatorFunctor + * @tparam T Concrete node type identifier + * \param instanceNamePrefix prefix of the node instance name (will be called "prefix_i" with i + * a unique number. + * \param nodeCategory Category of the node. + * \return true if the node creator is successfully added, false if not (e.g. due to a name + * collision). + */ + template + auto registerNodeCreator( const std::string& instanceNamePrefix, + const std::string& nodeCategory = "RadiumNodes" ) -> bool; + /** + * Associate, for a given concrete node type name, a NodeCreatorFunctor + * \param nodeType the name of the concrete type + * (the same as what is obtained by T::getTypename() on a node of type T) + * \param nodeCreator Functor to create an node of the corresponding concrete node type. + * \return true if the node creator is successfully added, false if not (e.g. due to a name + * collision). + */ + auto registerNodeCreator( const std::string& nodeType, + NodeCreatorFunctor nodeCreator, + const std::string& nodeCategory = "RadiumNodes" ) -> bool; + + /** + * Get an unique, increasing node id. + * \return + */ + auto nextNodeId() -> size_t; + + /** Create a node of the requested type. + * The node is filled with the given json content. + * If owningGraph is non null, the node is added to this graph + * \param nodeType Type name of the node to be created. + * \param data json representation of the node data (might be empty) + * \param owningGraph if non null, the node is added to ths graph + * \return the new node. Ownership of the returned pointer is left to the caller. + */ + [[nodiscard]] auto createNode( const std::string& nodeType, + const nlohmann::json& data, + DataflowGraph* owningGraph = nullptr ) -> Node*; + + /** + * The type of the associative container used to store the factory + * this container associate a concrete node type name to a pair + * + * The name of the node category is helpful for graphical NodeGraph editor. + * By default this is set to be "RadiumNodes" + */ + using ContainerType = + std::unordered_map>; + /** + * Get a const reference on the associative map + * \return + */ + [[nodiscard]] auto getFactoryMap() const -> const ContainerType&; + + private: + ContainerType m_nodesCreators; + size_t m_nodesCreated { 0 }; + std::string m_name; +}; + +/** + * NodeFactorySet store a set of NodeFactory + */ +class RA_DATAFLOW_API NodeFactorySet +{ + public: + using container_type = std::map>; + + using key_type = container_type::key_type; + using mapped_type = container_type::mapped_type; + using value_type = container_type::value_type; + + using const_iterator = container_type::const_iterator; + using iterator = container_type::iterator; + + /** + * Add a factory to the set of factories available + * \param factory the factory + * \return true if the factory was inserted, false if the insertion was prevented by an + * already existing factory with the same name. + */ + auto addFactory( mapped_type factory ) -> bool; + + /** + * \brief Test if a factory exists in the set with the given name + * \param name The name of the factory to search for + * \return an optional that is empty (evaluates to false) if no factory exists with the given + * name or that contains the existing factory. + */ + auto hasFactory( const key_type& name ) -> Ra::Core::Utils::optional; + + /** + * \brief Remove the identified factory from the set + * \param name the name of the factory to remove + * \return true if the factory was removed, false if the factory does not exist in the set. + */ + auto removeFactory( const key_type& name ) -> bool; + + /** + * + * \return the created node, nullptr if there is no construction functor registered for the + * type. + */ + /** + * \brief Create a node using one of the functor (if it exists) registered in one factory for + * the given type name. \param nodeType name of the node type (as simplified by Radium + * demangler) to create \param data json data to fill the created node \param owningGraph Graph + * in which the node should be added, if not nullptr. \return The created node, nullptr in case + * of failure + */ + [[nodiscard]] auto createNode( const std::string& nodeType, + const nlohmann::json& data, + DataflowGraph* owningGraph = nullptr ) -> Node*; + + /* Wrappers to the interface of the underlying container + * see https://en.cppreference.com/w/cpp/container/map + */ + auto begin() const -> const_iterator; + auto end() const -> const_iterator; + auto cbegin() const -> const_iterator; + auto cend() const -> const_iterator; + auto find( const key_type& key ) const -> const_iterator; + auto insert( value_type value ) -> std::pair; + auto erase( const key_type& key ) -> size_t; + + private: + container_type m_factories; +}; + +/** + * Implement a NodeFactoryManager that stores a set of factories available to the system. + * Such a manager will be populated with Core::Dataflow node factories (Specialized sources, + * specialized sink, ...) and will allow users to register its own factories. + * + * When creating a graph, the set of needed factories should be given as a constructor parameter + * or built by adding factories identifier to the graph. + * + * When a graph is saved, the name of the factories he needs will be exported as string array + * in the json. + * + * When a graph is loaded, the set of factories is built using the factories array in the json. + * + * @note: the factory name "DataFlowBuiltIns" is reserved and correspond to the base nodes available + * for each dataflow graph (Specialized sources, specialized sink, ...). This factory will be + * automatically added to all created factory set. + */ +namespace NodeFactoriesManager { +/** Names of the system Builtins factories (automatically added to each graph) */ +extern const std::string dataFlowBuiltInsFactoryName; + +RA_DATAFLOW_API auto getFactoryManager() -> NodeFactorySet&; + +/** Register a factory into the manager. + * The key will be fetched from the factory (its name) + */ +/** + * \brief Register a factory into the manager. + * The key will be fetched from the factory (its name) + * \param factory + * \return true if the factory was registered, false if not (e.g. due to name collision). + */ +RA_DATAFLOW_API auto registerFactory( NodeFactorySet::mapped_type factory ) -> bool; + +/** + * \brief Create and register a factory to the manager. + * \param name The name of the factory to create + * \return a configurable factory. + */ +RA_DATAFLOW_API auto createFactory( const NodeFactorySet::key_type& name ) + -> NodeFactorySet::mapped_type; + +/** + * \brief Gets the given factory from the manager + * \param name The name of the factory to get + * \return a shared_ptr to the requested factory, nullptr if the factory does not exist. + */ +RA_DATAFLOW_API auto getFactory( const NodeFactorySet::key_type& name ) + -> NodeFactorySet::mapped_type; + +/** + * \brief Unregister the factory from the manager + * \param name The name of the factory to unregister + * \return true if the factory was unregistered, false if not (e.g. for names not being managed). + */ +RA_DATAFLOW_API auto unregisterFactory( const NodeFactorySet::key_type& name ) -> bool; + +/** + * \brief Gets the factory for nodes exported by the Core dataflow library. + * \return + */ +RA_DATAFLOW_API auto getDataFlowBuiltInsFactory() -> NodeFactorySet::mapped_type; +} // namespace NodeFactoriesManager + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +auto NodeFactory::registerNodeCreator( NodeCreatorFunctor nodeCreator, + const std::string& nodeCategory ) -> bool { + return registerNodeCreator( T::getTypename(), std::move( nodeCreator ), nodeCategory ); +} + +template +auto NodeFactory::registerNodeCreator( const std::string& instanceNamePrefix, + const std::string& nodeCategory ) -> bool { + return registerNodeCreator( + T::getTypename(), + [this, instanceNamePrefix]( const nlohmann::json& data ) { + std::string instanceName; + if ( data.contains( "instance" ) ) { instanceName = data["instance"]; } + else { instanceName = instanceNamePrefix + std::to_string( this->nextNodeId() ); } + auto node = new T( instanceName ); + node->fromJson( data ); + return node; + }, + nodeCategory ); +} + +inline auto NodeFactory::getFactoryMap() const -> const NodeFactory::ContainerType& { + return m_nodesCreators; +} + +inline auto NodeFactorySet::addFactory( NodeFactorySet::mapped_type factory ) -> bool { + const auto [loc, inserted] = insert( { factory->getName(), std::move( factory ) } ); + return inserted; +} + +inline auto NodeFactorySet::hasFactory( const NodeFactorySet::key_type& name ) + -> Ra::Core::Utils::optional { + if ( auto fct = m_factories.find( name ); fct != m_factories.end() ) { return fct->second; } + return {}; +} + +inline auto NodeFactorySet::removeFactory( const NodeFactorySet::key_type& name ) -> bool { + return erase( name ); +} +inline auto NodeFactorySet::begin() const -> NodeFactorySet::const_iterator { + return m_factories.begin(); +} +inline auto NodeFactorySet::end() const -> NodeFactorySet::const_iterator { + return m_factories.end(); +} +inline auto NodeFactorySet::cbegin() const -> NodeFactorySet::const_iterator { + return m_factories.cbegin(); +} +inline auto NodeFactorySet::cend() const -> NodeFactorySet::const_iterator { + return m_factories.cend(); +} +inline auto NodeFactorySet::find( const NodeFactorySet::key_type& key ) const + -> NodeFactorySet::const_iterator { + return m_factories.find( key ); +} +inline auto NodeFactorySet::insert( NodeFactorySet::value_type value ) + -> std::pair { + return m_factories.insert( std::move( value ) ); +} +inline auto NodeFactorySet::erase( const NodeFactorySet::key_type& key ) -> size_t { + return m_factories.erase( key ); +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.cpp b/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.cpp new file mode 100644 index 00000000000..cd7a666cc71 --- /dev/null +++ b/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.cpp @@ -0,0 +1,36 @@ +#include +DATAFLOW_LIBRARY_INITIALIZER_DECL( CoreNodes ); + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace NodeFactoriesManager { + +void registerStandardFactories() { + if ( getFactory( NodeFactoriesManager::dataFlowBuiltInsFactoryName ) ) { return; } + auto coreFactory = createFactory( NodeFactoriesManager::dataFlowBuiltInsFactoryName ); + /* --- Sources --- */ + Private::registerSourcesFactories( coreFactory ); + + /* --- Sinks --- */ + Private::registerSinksFactories( coreFactory ); + + /* --- Functionals */ + Private::registerFunctionalsFactories( coreFactory ); + + /* --- Graphs --- */ + coreFactory->registerNodeCreator( DataflowGraph::getTypename() + "_", "Graph" ); +} +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra + +DATAFLOW_LIBRARY_INITIALIZER_IMPL( CoreNodes ) { + Ra::Dataflow::Core::NodeFactoriesManager::registerStandardFactories(); +} diff --git a/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.hpp b/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.hpp new file mode 100644 index 00000000000..c8fd82d811c --- /dev/null +++ b/src/Dataflow/Core/Nodes/CoreBuiltInsNodes.hpp @@ -0,0 +1,38 @@ +#pragma once +#include + +#include +namespace Ra { +namespace Dataflow { +namespace Core { +namespace NodeFactoriesManager { + +/** + * \brief Create the node system default factory. + * + * The default factory of the node system contains instantiation of the nodes below for the + * following type + * - Scalar, float, double int, unsigned int + * - Color, VectorDf, VectorDd, VectorDi, VectorDi (where D in {2, 3, 4} + * + * List of instanced nodes for any TYPE above + * - SingleDataSourceNode and SingleDataSourceNode> + * - FunctionSourceNode, FunctionSourceNode + * - FunctionSourceNode, FunctionSourceNode + * - SinkNode, SinkNode + * - FilterNode> + * - TransformNode>, ReduceNode> + * - BinaryOpNode, BinaryOpNode>, BinaryOpNode + * + * All these node might be serialized/unserialized without any additional nor custom factory. + * + * If needed, the definition of all these type aliases can be included using one of the headers + * - #include + * - #include + * - #include + */ +RA_DATAFLOW_API void registerStandardFactories(); +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Functionals/BinaryOpNode.hpp b/src/Dataflow/Core/Nodes/Functionals/BinaryOpNode.hpp new file mode 100644 index 00000000000..ad8156cfe7b --- /dev/null +++ b/src/Dataflow/Core/Nodes/Functionals/BinaryOpNode.hpp @@ -0,0 +1,255 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Functionals { + +/** + * \brief namespace containing template utilities for management of BinaryOpNode content + */ +namespace internal { + +/** + * \brief Type traits giving access to value_type and const ref type + * \tparam A + * \tparam is_container + */ +template +struct ArgTypeHelperInternal { + using value_type = A; + using const_value_ref = const A&; +}; + +/** + * \brief Partial specialization for container type + * \tparam A + */ +template +struct ArgTypeHelperInternal { + using value_type = typename A::value_type; + using const_value_ref = const typename A::value_type&; +}; + +/** + * \brief CRTP + * \tparam A + */ +template +struct ArgTypeHelper : public ArgTypeHelperInternal::value> {}; + +// Find a way to evaluate the copy/move semantic of t_out +/** + * \brief Manage the call to y = f(a, b) according to inputs aand ouput types of the node + * \tparam t_a Type of the source data for argument a of the function + * \tparam t_b Type of the source data for argument b of the function + * \tparam t_out Type of the ouput data sent by the node + * \tparam funcType Profile of the operator f + * \tparam it_a true if t_a is a container + * \tparam it_b true if t_b is a container + * \tparam it_out true if t_out is a container + */ +template ::value, + bool it_b = Ra::Core::Utils::is_container::value, + bool it_out = Ra::Core::Utils::is_container::value> +struct ExecutorHelper { + static t_out executeInternal( t_a&, t_b&, funcType ) { + static_assert( ( ( it_a || it_b ) ? it_out : !it_out ), "Invalid template parameter " ); + } +}; + +/** + * \brief Call of an operator to transform two container into another container. + */ +template +struct ExecutorHelper { + static t_out executeInternal( t_a& a, t_b& b, funcType f ) { + t_out res; + std::transform( a.begin(), + a.end(), + b.begin(), + std::back_inserter( res ), + [f]( typename ArgTypeHelper::const_value_ref x, + typename ArgTypeHelper::const_value_ref y ) -> + typename ArgTypeHelper::value_type { return f( x, y ); } ); + return res; + } +}; + +/** + * \brief Call of an operator to transform a container and a scalar into a container. + */ +template +struct ExecutorHelper { + static t_out executeInternal( t_a& a, t_b& b, funcType f ) { + t_out res; + std::transform( a.begin(), + a.end(), + std::back_inserter( res ), + [&b, f]( typename ArgTypeHelper::const_value_ref x ) -> + typename ArgTypeHelper::value_type { return f( x, b ); } ); + return res; + } +}; + +/** + * \brief Call of an operator to transform a scalar and a container into a container. + */ +template +struct ExecutorHelper { + static t_out executeInternal( t_a& a, t_b& b, funcType f ) { + t_out res; + std::transform( b.begin(), + b.end(), + std::back_inserter( res ), + [&a, f]( typename ArgTypeHelper::const_value_ref x ) -> + typename ArgTypeHelper::value_type { return f( a, x ); } ); + return res; + } +}; + +/** + * \brief Call of an operator to transform two scalars into a scalar. + */ +template +struct ExecutorHelper { + static t_out executeInternal( t_a& a, t_b& b, funcType f ) { return f( a, b ); } +}; +} // namespace internal + +/** \brief Apply a binary operation on its input. + * \tparam t_a type of the first argument + * \tparam t_b type of the second argument + * \tparam t_out type of the result + * + * This node apply an operator f on its input such that : + * - if t_a, t_b and t_out are collections, r[i] = f(a[i], b[i]) for all elements i in the + * collections. + * - if t_a and t_out are collections, t_b an object, r[i] = f(a[i], b) for all elements i in the + * collections. + * - if t_b and t_out are collections, t_a an object, r[i] = f(a, b[i]) for all elements i in the + * collections. + * - if t_a, t_b and t_out are objects, r = f(a, b). + * All other configurations of t_a, t_b and t_out are illegal. + * + * This node has three inputs : + * - a : port accepting the input data of type t_a. Must be linked. + * - b : port accepting the input data of type t_b. Must be linked. + * - f : port accepting an operator with profile std::function. + * Link to this port is not mandatory, the operator might be set once for the node. + * If this port is linked, the operator will be taken from the port. + * + * This node has one output : + * - out : port giving a t_out such that out = std::transform(a, b, f) + */ +template +class BinaryOpNode : public Node +{ + public: + /** + * BinaryOp operator profile + */ + using Arg1_type = typename internal::ArgTypeHelper::const_value_ref; + using Arg2_type = typename internal::ArgTypeHelper::const_value_ref; + using Res_type = typename internal::ArgTypeHelper::value_type; + using BinaryOperator = std::function; + + /** + * \brief Construct an null operator + * \param instanceName + */ + explicit BinaryOpNode( const std::string& instanceName ) : + BinaryOpNode( instanceName, getTypename(), []( Arg1_type, Arg2_type ) -> Res_type { + return Res_type {}; + } ) {} + + /** + * \brief Construct a BinaryOpNode with the given operator + * \param instanceName + * \param op + */ + BinaryOpNode( const std::string& instanceName, BinaryOperator op ) : + BinaryOpNode( instanceName, getTypename(), op ) {} + + void init() override { + m_result = t_out {}; + Node::init(); + } + + bool execute() override { + auto f = m_operator; + if ( m_portF->isLinked() ) { f = m_portF->getData(); } + // The following test will always be true if the node was integrated in a compiled graph + if ( m_portA->isLinked() && m_portB->isLinked() ) { + m_result = internal::ExecutorHelper::executeInternal( + m_portA->getData(), m_portB->getData(), f ); + } + return true; + } + + /// \brief Sets the operator to be evaluated by the node. + void setOperator( BinaryOperator op ) { m_operator = op; } + + protected: + BinaryOpNode( const std::string& instanceName, + const std::string& typeName, + BinaryOperator op ) : + Node( instanceName, typeName ), m_operator( op ) { + addInput( m_portA ); + m_portA->mustBeLinked(); + addInput( m_portB ); + m_portB->mustBeLinked(); + addInput( m_portF ); + addOutput( m_portR, &m_result ); + } + + void toJsonInternal( nlohmann::json& data ) const override { + data["comment"] = + std::string { "Binary operator could not be serialized for " } + getTypeName(); + LOG( Ra::Core::Utils::logDEBUG ) + << "Unable to save data when serializing a " << getTypeName() << "."; + } + + bool fromJsonInternal( const nlohmann::json& ) override { + LOG( Ra::Core::Utils::logDEBUG ) + << "Unable to read data when un-serializing a " << getTypeName() << "."; + return true; + } + + private: + /// \brief the used operator + BinaryOperator m_operator = []( Arg1_type, Arg2_type ) -> Res_type { return Res_type {}; }; + t_out m_result; + + /// @{ + /// \brief Alias for the ports (allow simpler access) + PortIn* m_portA { new PortIn( "a", this ) }; + PortIn* m_portB { new PortIn( "b", this ) }; + PortIn* m_portF { new PortIn( "f", this ) }; + PortOut* m_portR { new PortOut( "r", this ) }; + /// @} + + public: + static const std::string& getTypename() { + static std::string demangledName = + std::string { "BinaryOp<" } + Ra::Dataflow::Core::simplifiedDemangledType() + + " x " + Ra::Dataflow::Core::simplifiedDemangledType() + " -> " + + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledName; + } +}; + +// implementation of the methods are inlined +// see issue .inl coding style #1011 + +} // namespace Functionals +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Functionals/CoreDataFunctionals.hpp b/src/Dataflow/Core/Nodes/Functionals/CoreDataFunctionals.hpp new file mode 100644 index 00000000000..f93ebceba64 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Functionals/CoreDataFunctionals.hpp @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include + +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Functionals { + +using namespace Ra::Core; + +// This macro does not end with semicolon. To be added when calling it +#define DECLARE_FUNCTIONALS( SUFFIX, TYPE ) \ + using ArrayFilter##SUFFIX = FilterNode>; \ + using ArrayTransformer##SUFFIX = TransformNode>; \ + using ArrayReducer##SUFFIX = ReduceNode>; \ + using BinaryOp##SUFFIX = BinaryOpNode; \ + using BinaryOp##SUFFIX##Array = BinaryOpNode>; \ + using BinaryPredicate##SUFFIX = BinaryOpNode + +/* Not yet supported + using BinaryPredicate##SUFFIX##Array = BinaryOpNode, + Ra::Core::VectorArray, bool> +*/ + +DECLARE_FUNCTIONALS( Float, float ); +DECLARE_FUNCTIONALS( Double, double ); +DECLARE_FUNCTIONALS( Scalar, Scalar ); +DECLARE_FUNCTIONALS( Int, int ); +DECLARE_FUNCTIONALS( UInt, unsigned int ); +DECLARE_FUNCTIONALS( Color, Utils::Color ); +DECLARE_FUNCTIONALS( Vector2f, Vector2f ); +DECLARE_FUNCTIONALS( Vector2d, Vector2d ); +DECLARE_FUNCTIONALS( Vector3f, Vector3f ); +DECLARE_FUNCTIONALS( Vector3d, Vector3d ); +DECLARE_FUNCTIONALS( Vector4f, Vector4f ); +DECLARE_FUNCTIONALS( Vector4d, Vector4d ); +DECLARE_FUNCTIONALS( Vector2i, Vector2i ); +DECLARE_FUNCTIONALS( Vector3i, Vector3i ); +DECLARE_FUNCTIONALS( Vector4i, Vector4i ); +DECLARE_FUNCTIONALS( Vector2ui, Vector2ui ); +DECLARE_FUNCTIONALS( Vector3ui, Vector3ui ); +DECLARE_FUNCTIONALS( Vector4ui, Vector4ui ); + +// TODO, instanciate nodes for otherypes ? + +#undef DECLARE_FUNCTIONALS +} // namespace Functionals +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Functionals/FilterNode.hpp b/src/Dataflow/Core/Nodes/Functionals/FilterNode.hpp new file mode 100644 index 00000000000..f72f85ca906 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Functionals/FilterNode.hpp @@ -0,0 +1,151 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Functionals { + +/** \brief Filter on iterable collection. + * \tparam coll_t the collection to filter. Must respect the SequenceContainer requirements + * \tparam v_t (optional), type of the element in the collection. Default to coll_t::value_type + * \see https://en.cppreference.com/w/cpp/named_req/SequenceContainer + * + * This node apply an operator f on its input such that to keep only elements validated by a + * predicate : + * + * This node has two inputs : + * - in : port accepting the input data of type coll_t. Must be linked. + * - f : port accepting an operator with profile std::function. + * Link to this port is not mandatory, the operator might be set once for the node. + * If this port is linked, the operator will be taken from the port. + * + * This node has one output : + * - out : port giving a coll_t such that out = std::copy_if(a, f) + */ +template +class FilterNode : public Node +{ + public: + /** + * unaryPredicate Type + */ + using UnaryPredicate = std::function; + + /** + * \brief Construct a filter accepting all its input ( true() lambda ) + * \param instanceName + */ + explicit FilterNode( const std::string& instanceName ); + + /** + * \brief Construct a filter with the given predicate + * \param instanceName + * \param predicate + */ + FilterNode( const std::string& instanceName, UnaryPredicate predicate ); + + void init() override; + bool execute() override; + + /// Sets the filtering predicate on the node + void setFilterFunction( UnaryPredicate predicate ); + + protected: + FilterNode( const std::string& instanceName, + const std::string& typeName, + UnaryPredicate predicate ); + + void toJsonInternal( nlohmann::json& data ) const override; + bool fromJsonInternal( const nlohmann::json& ) override; + + private: + UnaryPredicate m_predicate; + coll_t m_elements; + + /// @{ + /// \brief Alias for the ports (allow simpler access) + PortIn* m_portIn { new PortIn( "in", this ) }; + PortIn* m_portPredicate { new PortIn( "f", this ) }; + PortOut* m_portOut { new PortOut( "out", this ) }; + /// @} + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +FilterNode::FilterNode( const std::string& instanceName ) : + FilterNode( instanceName, getTypename(), []( v_t ) { return true; } ) {} + +template +FilterNode::FilterNode( const std::string& instanceName, UnaryPredicate predicate ) : + FilterNode( instanceName, getTypename(), predicate ) {} + +template +void FilterNode::setFilterFunction( UnaryPredicate predicate ) { + m_predicate = predicate; +} + +template +void FilterNode::init() { + Node::init(); + m_elements.clear(); +} + +template +bool FilterNode::execute() { + auto f = m_portPredicate->isLinked() ? m_portPredicate->getData() : m_predicate; + // The following test will always be true if the node was integrated in a compiled graph + if ( m_portIn->isLinked() ) { + const auto& inData = m_portIn->getData(); + m_elements.clear(); + // m_elements.reserve( inData.size() ); // --> this is not a requirement of + // SequenceContainer + std::copy_if( inData.begin(), inData.end(), std::back_inserter( m_elements ), f ); + } + return true; +} + +template +const std::string& FilterNode::getTypename() { + static std::string demangledName = + std::string { "Filter<" } + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledName; +} + +template +FilterNode::FilterNode( const std::string& instanceName, + const std::string& typeName, + UnaryPredicate filterFunction ) : + Node( instanceName, typeName ), m_predicate( filterFunction ) { + + addInput( m_portIn ); + m_portIn->mustBeLinked(); + addInput( m_portPredicate ); + addOutput( m_portOut, &m_elements ); +} + +template +void FilterNode::toJsonInternal( nlohmann::json& data ) const { + data["comment"] = + std::string { "Filtering function could not be serialized for " } + getTypeName(); + LOG( Ra::Core::Utils::logWARNING ) // TODO make this logDEBUG + << "Unable to save data when serializing a " << getTypeName() << "."; +} + +template +bool FilterNode::fromJsonInternal( const nlohmann::json& ) { + LOG( Ra::Core::Utils::logWARNING ) // TODO make this logDEBUG + << "Unable to read data when un-serializing a " << getTypeName() << "."; + return true; +} + +} // namespace Functionals +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Functionals/ReduceNode.hpp b/src/Dataflow/Core/Nodes/Functionals/ReduceNode.hpp new file mode 100644 index 00000000000..15b34e279b5 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Functionals/ReduceNode.hpp @@ -0,0 +1,168 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Functionals { + +/** \brief Reduce an iterable collection using a given operator + * \tparam coll_t the collection to reduce. Must respect the SequenceContainer requirements + * \tparam v_t (optional), type of the element in the collection. Default to coll_t::value_type + * \see https://en.cppreference.com/w/cpp/named_req/SequenceContainer + * + * This node has three inputs : + * - in : port accepting a coll_t data. Linking to this port is mandatory + * - f : port accepting an operator with profile std::function. + * Linking to this port is not mandatory, the operator might be set once for the node or is the + * default value for v_t. If this port is linked, the operator will be taken from the port. + * - init : port accepting a v_t to serve as initial value. + * Linking to this port is not mandatory. + * If this port is linked, the initial value will be taken from the port. + * + * This node has one output : + * - out : port giving a v_t such that out = std::reduce(in, f, init) (std::accumulate if less + * than C++17) + */ +// TODO, allow to specify the type of the reduced information. This will allow, e.g, given a +// collection of X, to compute a tuple containing mean and standard deviation of the collection +// as reduction (using online Welford algo). +template +class ReduceNode : public Node +{ + public: + /** + * Trasformation operator profile + */ + using ReduceOperator = std::function; + + /** + * \brief Construct an identity transformer + * \param instanceName + */ + explicit ReduceNode( const std::string& instanceName ); + + /** + * \brief Construct a transformer with the given operator + * \param instanceName + * \param op + * \param initialValue + */ + ReduceNode( const std::string& instanceName, ReduceOperator op, v_t initialValue = v_t {} ); + + void init() override; + bool execute() override; + + /// Sets the operator on the node + void setOperator( ReduceOperator op, v_t initialValue = v_t {} ); + + protected: + ReduceNode( const std::string& instanceName, + const std::string& typeName, + ReduceOperator op, + v_t initialValue ); + + void toJsonInternal( nlohmann::json& data ) const override; + bool fromJsonInternal( const nlohmann::json& ) override; + + private: + ReduceOperator m_operator; + v_t m_result; + v_t m_init; + + /// @{ + /// \brief Alias for the ports (allow simpler access) + PortIn* m_portIn { new PortIn( "in", this ) }; + PortIn* m_portF { new PortIn( "f", this ) }; + PortIn* m_portInit { new PortIn( "init", this ) }; + PortOut* m_portOut { new PortOut( "out", this ) }; + /// @} + + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +ReduceNode::ReduceNode( const std::string& instanceName ) : + ReduceNode( + instanceName, + getTypename(), + []( const v_t& a, const v_t& ) -> v_t { return a; }, + v_t {} ) {} + +template +ReduceNode::ReduceNode( const std::string& instanceName, + ReduceOperator op, + v_t initialValue ) : + ReduceNode( instanceName, getTypename(), op, initialValue ) {} + +template +void ReduceNode::setOperator( ReduceOperator op, v_t initialValue ) { + m_operator = op; + m_init = initialValue; +} + +template +void ReduceNode::init() { + Node::init(); + m_result = m_init; +} + +template +bool ReduceNode::execute() { + auto f = m_portF->isLinked() ? m_portF->getData() : m_operator; + auto iv = m_portInit->isLinked() ? m_portInit->getData() : m_init; + m_result = iv; + // The following test will always be true if the node was integrated in a compiled graph + if ( m_portIn->isLinked() ) { + const auto& inData = m_portIn->getData(); + m_result = std::accumulate( inData.begin(), inData.end(), iv, f ); + } + return true; +} + +template +const std::string& ReduceNode::getTypename() { + static std::string demangledName = + std::string { "Reduce<" } + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledName; +} + +template +ReduceNode::ReduceNode( const std::string& instanceName, + const std::string& typeName, + ReduceOperator op, + v_t initialValue ) : + Node( instanceName, typeName ), m_operator( op ), m_init( initialValue ) { + + addInput( m_portIn ); + m_portIn->mustBeLinked(); + addInput( m_portF ); + addInput( m_portInit ); + addOutput( m_portOut, &m_result ); +} + +template +void ReduceNode::toJsonInternal( nlohmann::json& data ) const { + data["comment"] = + std::string { "Reduce operator could not be serialized for " } + getTypeName(); + LOG( Ra::Core::Utils::logWARNING ) // TODO make this logDEBUG + << "Unable to save data when serializing a " << getTypeName() << "."; +} + +template +bool ReduceNode::fromJsonInternal( const nlohmann::json& ) { + LOG( Ra::Core::Utils::logWARNING ) // TODO make this logDEBUG + << "Unable to read data when un-serializing a " << getTypeName() << "."; + return true; +} + +} // namespace Functionals +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Functionals/TransformNode.hpp b/src/Dataflow/Core/Nodes/Functionals/TransformNode.hpp new file mode 100644 index 00000000000..c2b1763de56 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Functionals/TransformNode.hpp @@ -0,0 +1,147 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Functionals { + +/** \brief Transform an iterable collection + * \tparam coll_t the collection to transform. Must respect the SequenceContainer requirements + * \tparam v_t (optional), type of the element in the collection. Default to coll_t::value_type + * \see https://en.cppreference.com/w/cpp/named_req/SequenceContainer + * + * This node has two inputs : + * - in : port accepting a coll_t data. Linking to this port is mandatory + * - f : port accepting an operator with profile std::function. + * Linking to this port is not mandatory, the operator might be set once for the node. + * If this port is linked, the operator will be taken from the port. + * + * This node has one output : + * - out : port giving a coll_t such that out = std::transform(in, f) + */ +template +class TransformNode : public Node +{ + public: + /** + * Trasformation operator profile + */ + using TransformOperator = std::function; + + /** + * \brief Construct an identity transformer + * \param instanceName + */ + explicit TransformNode( const std::string& instanceName ); + + /** + * \brief Construct a transformer with the given operator + * \param instanceName + * \param filterFunction + */ + TransformNode( const std::string& instanceName, TransformOperator op ); + + void init() override; + bool execute() override; + + /// Sets the operator on the node + void setOperator( TransformOperator op ); + + protected: + TransformNode( const std::string& instanceName, + const std::string& typeName, + TransformOperator op ); + + void toJsonInternal( nlohmann::json& data ) const override; + bool fromJsonInternal( const nlohmann::json& ) override; + + private: + TransformOperator m_operator; + coll_t m_elements; + + /// @{ + /// \brief Alias for the ports (allow simpler access) + PortIn* m_portIn { new PortIn( "in", this ) }; + PortIn* m_portOperator { new PortIn( "f", this ) }; + PortOut* m_portOut { new PortOut( "out", this ) }; + /// @} + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +TransformNode::TransformNode( const std::string& instanceName ) : + TransformNode( instanceName, getTypename(), []( v_t ) { return v_t {}; } ) {} + +template +TransformNode::TransformNode( const std::string& instanceName, TransformOperator op ) : + TransformNode( instanceName, getTypename(), op ) {} + +template +void TransformNode::setOperator( TransformOperator op ) { + m_operator = op; +} + +template +void TransformNode::init() { + Node::init(); + m_elements.clear(); +} + +template +bool TransformNode::execute() { + auto f = m_portOperator->isLinked() ? m_portOperator->getData() : m_operator; + // The following test will always be true if the node was integrated in a compiled graph + if ( m_portIn->isLinked() ) { + const auto& inData = m_portIn->getData(); + m_elements.clear(); + // m_elements.reserve( inData.size() ); // --> this is not a requirement of + // SequenceContainer + std::transform( inData.begin(), inData.end(), std::back_inserter( m_elements ), f ); + } + return true; +} + +template +const std::string& TransformNode::getTypename() { + static std::string demangledName = + std::string { "Transform<" } + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledName; +} + +template +TransformNode::TransformNode( const std::string& instanceName, + const std::string& typeName, + TransformOperator op ) : + Node( instanceName, typeName ), m_operator( op ) { + addInput( m_portIn ); + m_portIn->mustBeLinked(); + addInput( m_portOperator ); + addOutput( m_portOut, &m_elements ); +} + +template +void TransformNode::toJsonInternal( nlohmann::json& data ) const { + data["comment"] = + std::string { "Transform operator could not be serialized for " } + getTypeName(); + LOG( Ra::Core::Utils::logWARNING ) // TODO make this logDEBUG + << "Unable to save data when serializing a " << getTypeName() << "."; +} + +template +bool TransformNode::fromJsonInternal( const nlohmann::json& ) { + LOG( Ra::Core::Utils::logWARNING ) // TODO make this logDEBUG + << "Unable to read data when un-serializing a " << getTypeName() << "."; + return true; +} + +} // namespace Functionals +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.cpp b/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.cpp new file mode 100644 index 00000000000..f2b5bae2733 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.cpp @@ -0,0 +1,61 @@ +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace NodeFactoriesManager { + +namespace Private { + +/** TODO : replace this by factory autoregistration at compile time */ +#define ADD_FUNCTIONALS_TO_FACTORY( FACTORY, NAMESPACE, SUFFIX ) \ + FACTORY->registerNodeCreator( \ + NAMESPACE::ArrayFilter##SUFFIX::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::ArrayTransformer##SUFFIX::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::ArrayReducer##SUFFIX::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::BinaryOp##SUFFIX::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::BinaryOp##SUFFIX##Array::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::BinaryPredicate##SUFFIX::getTypename() + "_", #NAMESPACE ) + +/* + * not yet supported + FACTORY->registerNodeCreator( \ + NAMESPACE::BinaryPredicate##SUFFIX##Array::getTypename() + "_", #NAMESPACE ) +*/ + +void registerFunctionalsFactories( NodeFactorySet::mapped_type factory ) { + /* --- Functionals */ +#ifdef CORE_USE_DOUBLE + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Float ); +#else + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Double ); +#endif + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Scalar ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Int ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, UInt ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Color ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector2f ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector2d ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector3f ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector3d ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector4f ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector4d ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector2i ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector2ui ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector3i ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector3ui ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector4i ); + ADD_FUNCTIONALS_TO_FACTORY( factory, Functionals, Vector4ui ); +} +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.hpp b/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.hpp new file mode 100644 index 00000000000..0d02ecdb766 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/FunctionalsNodeFactory.hpp @@ -0,0 +1,18 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace NodeFactoriesManager { +namespace Private { + +void registerFunctionalsFactories( NodeFactorySet::mapped_type factory ); + +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.cpp b/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.cpp new file mode 100644 index 00000000000..e1fa6713761 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.cpp @@ -0,0 +1,53 @@ +#include +#include +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace NodeFactoriesManager { + +namespace Private { + +/** TODO : replace this by factory autoregistration at compile time */ + +#define ADD_SINKS_TO_FACTORY( FACTORY, NAMESPACE, PREFIX ) \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##Sink::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##ArraySink::getTypename() + "_", #NAMESPACE ) + +void registerSinksFactories( NodeFactorySet::mapped_type factory ) { + /* --- Sinks --- */ + // bool could not be declared as others, because of the specificity of std::vector that is + // not compatible with Ra::Core::VectorArray implementation see + // https://en.cppreference.com/w/cpp/container/vector_bool Right now, there is no + // Ra::Core::VectorArray of bool + factory->registerNodeCreator( Sinks::BooleanSink::getTypename() + "_", + "Sinks" ); +#ifdef CORE_USE_DOUBLE + ADD_SINKS_TO_FACTORY( factory, Sinks, Float ); +#else + ADD_SINKS_TO_FACTORY( factory, Sinks, Double ); +#endif + ADD_SINKS_TO_FACTORY( factory, Sinks, Scalar ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Int ); + ADD_SINKS_TO_FACTORY( factory, Sinks, UInt ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Color ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector2f ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector2d ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector3f ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector3d ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector4f ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector4d ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector2i ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector2ui ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector3i ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector3ui ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector4i ); + ADD_SINKS_TO_FACTORY( factory, Sinks, Vector4ui ); +} +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.hpp b/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.hpp new file mode 100644 index 00000000000..a048cb71cff --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/SinksNodeFactory.hpp @@ -0,0 +1,18 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace NodeFactoriesManager { +namespace Private { + +void registerSinksFactories( NodeFactorySet::mapped_type factory ); + +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.cpp b/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.cpp new file mode 100644 index 00000000000..ab9e2d50611 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.cpp @@ -0,0 +1,62 @@ +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace NodeFactoriesManager { + +namespace Private { + +/** TODO : replace this by factory autoregistration at compile time */ +#define ADD_SOURCES_TO_FACTORY( FACTORY, NAMESPACE, PREFIX ) \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##Source::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##ArraySource::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##UnaryFunctionSource::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##BinaryFunctionSource::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##UnaryPredicateSource::getTypename() + "_", #NAMESPACE ); \ + FACTORY->registerNodeCreator( \ + NAMESPACE::PREFIX##BinaryPredicateSource::getTypename() + "_", #NAMESPACE ) + +void registerSourcesFactories( NodeFactorySet::mapped_type factory ) { + /* --- Sources --- */ + // bool could not be declared as others, because of the specificity of std::vector that is + // not compatible with Ra::Core::VectorArray implementation see + // https://en.cppreference.com/w/cpp/container/vector_bool Right now, there is no + // Ra::Core::VectorArray of bool + factory->registerNodeCreator( + Sources::BooleanSource::getTypename() + "_", "Sources" ); + // prevent Scalar type collision with float or double in factory +#ifdef CORE_USE_DOUBLE + ADD_SOURCES_TO_FACTORY( factory, Sources, Float ); +#else + ADD_SOURCES_TO_FACTORY( factory, Sources, Double ); +#endif + ADD_SOURCES_TO_FACTORY( factory, Sources, Scalar ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Int ); + ADD_SOURCES_TO_FACTORY( factory, Sources, UInt ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Color ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector2f ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector2d ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector3f ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector3d ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector4f ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector4d ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector2i ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector2ui ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector3i ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector3ui ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector4i ); + ADD_SOURCES_TO_FACTORY( factory, Sources, Vector4ui ); +} +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.hpp b/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.hpp new file mode 100644 index 00000000000..f40f2c50151 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Private/SourcesNodeFactory.hpp @@ -0,0 +1,18 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace NodeFactoriesManager { +namespace Private { + +void registerSourcesFactories( NodeFactorySet::mapped_type factory ); + +} // namespace Private +} // namespace NodeFactoriesManager +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Sinks/CoreDataSinks.hpp b/src/Dataflow/Core/Nodes/Sinks/CoreDataSinks.hpp new file mode 100644 index 00000000000..854bc12e2f4 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Sinks/CoreDataSinks.hpp @@ -0,0 +1,47 @@ +#pragma once +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Sinks { + +using namespace Ra::Core; + +#define DECLARE_SINKS( PREFIX, TYPE ) \ + using PREFIX##Sink = SinkNode; \ + using PREFIX##ArraySink = SinkNode> + +// bool could not be declared as others, because of the specificity of std::vector that is not +// compatible with Ra::Core::VectorArray implementation see +// https://en.cppreference.com/w/cpp/container/vector_bool Right now, there is no +// Ra::Core::VectorArray of bool +using BooleanSink = SinkNode; +DECLARE_SINKS( Float, float ); +DECLARE_SINKS( Double, double ); +DECLARE_SINKS( Scalar, Scalar ); +DECLARE_SINKS( Int, int ); +DECLARE_SINKS( UInt, unsigned int ); +DECLARE_SINKS( Color, Utils::Color ); +DECLARE_SINKS( Vector2f, Vector2f ); +DECLARE_SINKS( Vector2d, Vector2d ); +DECLARE_SINKS( Vector3f, Vector3f ); +DECLARE_SINKS( Vector3d, Vector3d ); +DECLARE_SINKS( Vector4f, Vector4f ); +DECLARE_SINKS( Vector4d, Vector4d ); +DECLARE_SINKS( Vector2i, Vector2i ); +DECLARE_SINKS( Vector3i, Vector3i ); +DECLARE_SINKS( Vector4i, Vector4i ); +DECLARE_SINKS( Vector2ui, Vector2ui ); +DECLARE_SINKS( Vector3ui, Vector3ui ); +DECLARE_SINKS( Vector4ui, Vector4ui ); + +#undef DECLARE_SINKS +} // namespace Sinks +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Sinks/SinkNode.hpp b/src/Dataflow/Core/Nodes/Sinks/SinkNode.hpp new file mode 100644 index 00000000000..7b50fe9e006 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Sinks/SinkNode.hpp @@ -0,0 +1,114 @@ +#pragma once +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Sinks { + +/** + * \brief Base class for nodes that will store the result of a computation graph. + * @tparam T The type of the data to serve. + */ +template +class SinkNode : public Node +{ + protected: + SinkNode( const std::string& instanceName, const std::string& typeName ); + + public: + explicit SinkNode( const std::string& name ) : SinkNode( name, SinkNode::getTypename() ) {} + + /** + * \brief initialize the interface port data pointer + */ + void init() override; + bool execute() override; + + /** + * Get the delivered data + * @return a copy of the delivered data. + */ + T getData() const; + /** + * Get the delivered data + * @return a const ref to the delivered data. + */ + const T& getDataByRef() const; + + protected: + void toJsonInternal( nlohmann::json& data ) const override; + bool fromJsonInternal( const nlohmann::json& data ) override; + + private: + /// \todo : allow user to specify where to store the data ? (i.e. make this a shared_ptr ?). + // If yes, add a method setDataStorage(std::shared_ptr v) + T m_data; + + /// @{ + /// \brief Alias for the ports (allow simpler access) + PortIn* m_portIn { new PortIn( "from", this ) }; + /// @} + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +SinkNode::SinkNode( const std::string& instanceName, const std::string& typeName ) : + Node( instanceName, typeName ) { + + m_portIn->mustBeLinked(); + addInput( m_portIn ); +} + +template +void SinkNode::init() { + // this should be done only once (or when the address of local data changes) + if ( m_interface[0] != nullptr ) { + auto interfacePort = static_cast*>( m_interface[0] ); + interfacePort->setData( &m_data ); + } + Node::init(); +} + +template +bool SinkNode::execute() { + if ( m_portIn->hasData() ) { + m_data = m_portIn->getData(); + return true; + } + return false; +} + +template +T SinkNode::getData() const { + return m_data; +} + +template +const T& SinkNode::getDataByRef() const { + return m_data; +} + +template +const std::string& SinkNode::getTypename() { + static std::string demangledName = + std::string { "Sink<" } + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledName; +} + +template +void SinkNode::toJsonInternal( nlohmann::json& ) const {} + +template +bool SinkNode::fromJsonInternal( const nlohmann::json& ) { + return true; +} + +} // namespace Sinks +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Sources/CoreDataSources.hpp b/src/Dataflow/Core/Nodes/Sources/CoreDataSources.hpp new file mode 100644 index 00000000000..34593706eba --- /dev/null +++ b/src/Dataflow/Core/Nodes/Sources/CoreDataSources.hpp @@ -0,0 +1,184 @@ +#pragma once +#include +#include + +#include +#include +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Sources { +// TODO unify editable and non editable data sources + +// declare synonyms for convenient sources +// This macro does not end with semicolon. To be added when calling it +#define DECLARE_COREDATA_SOURCES( PREFIX, TYPE ) \ + using PREFIX##Source = SingleDataSourceNode; \ + using PREFIX##ArraySource = SingleDataSourceNode>; \ + using PREFIX##UnaryFunctionSource = FunctionSourceNode; \ + using PREFIX##BinaryFunctionSource = FunctionSourceNode; \ + using PREFIX##UnaryPredicateSource = FunctionSourceNode; \ + using PREFIX##BinaryPredicateSource = FunctionSourceNode; + +using namespace Ra::Core; + +// bool could not be declared as others, because of the specificity of std::vector that is not +// compatible with Ra::Core::VectorArray implementation see +// https://en.cppreference.com/w/cpp/container/vector_bool Right now, there is no +// Ra::Core::VectorArray of bool +using BooleanSource = SingleDataSourceNode; +DECLARE_COREDATA_SOURCES( Float, float ) +DECLARE_COREDATA_SOURCES( Double, double ) +DECLARE_COREDATA_SOURCES( Scalar, Scalar ) +DECLARE_COREDATA_SOURCES( Int, int ) +DECLARE_COREDATA_SOURCES( UInt, unsigned int ) +DECLARE_COREDATA_SOURCES( Color, Utils::Color ) +DECLARE_COREDATA_SOURCES( Vector2f, Vector2f ) +DECLARE_COREDATA_SOURCES( Vector2d, Vector2d ) +DECLARE_COREDATA_SOURCES( Vector3f, Vector3f ) +DECLARE_COREDATA_SOURCES( Vector3d, Vector3d ) +DECLARE_COREDATA_SOURCES( Vector4f, Vector4f ) +DECLARE_COREDATA_SOURCES( Vector4d, Vector4d ) +DECLARE_COREDATA_SOURCES( Vector2i, Vector2i ) +DECLARE_COREDATA_SOURCES( Vector3i, Vector3i ) +DECLARE_COREDATA_SOURCES( Vector4i, Vector4i ) +DECLARE_COREDATA_SOURCES( Vector2ui, Vector2ui ) +DECLARE_COREDATA_SOURCES( Vector3ui, Vector3ui ) +DECLARE_COREDATA_SOURCES( Vector4ui, Vector4ui ) + +#undef DECLARE_COREDATA_SOURCES + +// Partial specialisation for editable data sources +#define SPECIALIZE_EDITABLE_SOURCE( TYPE, NAME ) \ + template <> \ + inline SingleDataSourceNode::SingleDataSourceNode( const std::string& name ) : \ + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) { \ + setEditable( #NAME ); \ + } \ + \ + template <> \ + inline void SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { \ + data[#NAME] = *getData(); \ + } \ + \ + template <> \ + inline bool SingleDataSourceNode::fromJsonInternal( const nlohmann::json& data ) { \ + if ( data.contains( #NAME ) ) { \ + TYPE v = data[#NAME]; \ + setData( v ); \ + } \ + return true; \ + } + +SPECIALIZE_EDITABLE_SOURCE( bool, boolean ) +SPECIALIZE_EDITABLE_SOURCE( float, number ) +SPECIALIZE_EDITABLE_SOURCE( double, number ) +SPECIALIZE_EDITABLE_SOURCE( int, value ) +SPECIALIZE_EDITABLE_SOURCE( unsigned int, value ) + +#undef SPECIALIZE_EDITABLE_SOURCE + +// Color specialization need different implementation (as well as any Ra::Vectorxx) +template <> +inline SingleDataSourceNode::SingleDataSourceNode( + const std::string& name ) : + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) { + setEditable( "color" ); +} + +template <> +inline void +SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { + data["color"] = Ra::Core::Utils::Color::linearRGBTosRGB( *getData() ); +} + +template <> +inline bool +SingleDataSourceNode::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "color" ) ) { + std::array c = data["color"]; + auto v = + Ra::Core::Utils::Color::sRGBToLinearRGB( Ra::Core::Utils::Color( c[0], c[1], c[2] ) ); + setData( v ); + } + return true; +} + +// Ra::Core::Vector4 specialization +template <> +inline SingleDataSourceNode::SingleDataSourceNode( const std::string& name ) : + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) { + setEditable( "vector" ); +} + +template <> +inline void SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { + data["vector"] = *getData(); +} + +template <> +inline bool +SingleDataSourceNode::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "vector" ) ) { + std::array v = data["vector"]; + auto vec = Ra::Core::Vector4( v[0], v[1], v[2], v[3] ); + setData( vec ); + } + return true; +} + +// Ra::Core::Vector3 specialization +template <> +inline SingleDataSourceNode::SingleDataSourceNode( const std::string& name ) : + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) { + setEditable( "vector" ); +} + +template <> +inline void SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { + data["vector"] = *getData(); +} + +template <> +inline bool +SingleDataSourceNode::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "vector" ) ) { + std::array v = data["vector"]; + auto vec = Ra::Core::Vector3( v[0], v[1], v[2] ); + setData( vec ); + } + return true; +} + +// Ra::Core::Vector2 specialization +template <> +inline SingleDataSourceNode::SingleDataSourceNode( const std::string& name ) : + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) { + setEditable( "vector" ); +} + +template <> +inline void SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { + data["vector"] = *getData(); +} + +template <> +inline bool +SingleDataSourceNode::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "vector" ) ) { + std::array v = data["vector"]; + auto vec = Ra::Core::Vector2( v[0], v[1] ); + setData( vec ); + } + return true; +} + +} // namespace Sources +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Sources/FunctionSource.hpp b/src/Dataflow/Core/Nodes/Sources/FunctionSource.hpp new file mode 100644 index 00000000000..1d99e2331a6 --- /dev/null +++ b/src/Dataflow/Core/Nodes/Sources/FunctionSource.hpp @@ -0,0 +1,118 @@ +#pragma once +#pragma once +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Sources { + +/** + * \brief Node that deliver a std::function + * \tparam R return type of the function + * \tparam Type of the function arguments + */ +template +class FunctionSourceNode : public Node +{ + + public: + using function_type = std::function; + + explicit FunctionSourceNode( const std::string& name ) : + FunctionSourceNode( name, FunctionSourceNode::getTypename() ) {} + + bool execute() override; + + /** \brief Set the function to be delivered by the node. + * @param data + */ + void setData( function_type* data ); + + /** + * \brief Get the delivered data + * @return The non owning pointer (alias) to the delivered data. + */ + function_type* getData() const; + + protected: + FunctionSourceNode( const std::string& instanceName, const std::string& typeName ); + + bool fromJsonInternal( const nlohmann::json& ) override; + void toJsonInternal( nlohmann::json& data ) const override; + + /// @{ + /// The data provided by the node + function_type m_localData { []( Args... ) { return R {}; } }; + function_type* m_data { &m_localData }; + /// @} + + /// Alias to the output port + PortOut* m_portOut { new PortOut( "f", this ) }; + + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +FunctionSourceNode::FunctionSourceNode( const std::string& instanceName, + const std::string& typeName ) : + Node( instanceName, typeName ) { + addOutput( m_portOut, m_data ); +} + +template +bool FunctionSourceNode::execute() { + auto interfacePort = static_cast*>( m_interface[0] ); + if ( interfacePort->isLinked() ) { m_data = &( interfacePort->getData() ); } + else { m_data = &m_localData; } + m_portOut->setData( m_data ); + return true; +} + +template +void FunctionSourceNode::setData( function_type* data ) { + m_localData = *data; + m_data = &m_localData; + m_portOut->setData( m_data ); +} + +template +typename FunctionSourceNode::function_type* +FunctionSourceNode::getData() const { + return m_data; +} + +template +const std::string& FunctionSourceNode::getTypename() { + static std::string demangledTypeName = + std::string { "Source<" } + Ra::Dataflow::Core::simplifiedDemangledType() + + ">"; + return demangledTypeName; +} + +template +void FunctionSourceNode::toJsonInternal( nlohmann::json& data ) const { + data["comment"] = std::string( "Unable to save data when serializing a FunctionSourceNode<" ) + + Ra::Dataflow::Core::simplifiedDemangledType() + ">."; + LOG( Ra::Core::Utils::logDEBUG ) + << "Unable to save data when serializing a " << getTypeName() << "."; +} + +template +bool FunctionSourceNode::fromJsonInternal( const nlohmann::json& ) { + LOG( Ra::Core::Utils::logDEBUG ) + << "Unable to read data when un-serializing a " << getTypeName() << "."; + return true; +} + +} // namespace Sources +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Nodes/Sources/SingleDataSourceNode.hpp b/src/Dataflow/Core/Nodes/Sources/SingleDataSourceNode.hpp new file mode 100644 index 00000000000..12c4b4d3dcb --- /dev/null +++ b/src/Dataflow/Core/Nodes/Sources/SingleDataSourceNode.hpp @@ -0,0 +1,161 @@ +#pragma once +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +namespace Sources { +/** + * \brief Base class for nodes that will give access to some input data to the graph. + * This class can be used to feed nodes on a dataflow graph with some data coming + * from outside the graph or from the source node itself. + * + * The data delivered by the node can be explicitly set/get or can be made editable. + * + * @tparam T The type of the data to serve. + */ +template +class SingleDataSourceNode : public Node +{ + protected: + SingleDataSourceNode( const std::string& instanceName, const std::string& typeName ); + + public: + explicit SingleDataSourceNode( const std::string& name ) : + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) {} + + bool execute() override; + + /** \brief Set the data to be delivered by the node. + * @param data + * \warning This will copy the given data into the node. + * To prevent copy prefer using the corresponding dataSetter on the owning graph. + */ + void setData( T* data ); + + /** + * \brief Get the delivered data + * @return The non owning pointer (alias) to the delivered data. + */ + T* getData() const; + + /** + * \brief Set the delivered data editable using the given name + * Give access to the internal data storage. + * If the node interface is connected, the edition will not result on a propagation to the + * graph as internal data storage will be superseeded by the data from the interface. + * @param name Name of the data as it will appear on edition gui. If not given, the default + * name "Data" will be used. + */ + void setEditable( const std::string& name = "Data" ); + + /** + * \brief Remove the delivered data from being editable + * @param name Name of the data given when calling setEditable + */ + void removeEditable( const std::string& name = "Data" ); + + protected: + bool fromJsonInternal( const nlohmann::json& ) override; + void toJsonInternal( nlohmann::json& data ) const override; + + /// @{ + /// The data provided by the node + /// Used to deliver (and edit) data when the interface is not connected. + T m_localData; + /// Ownership of this pointer is left to the caller + T* m_data { &m_localData }; + /// @} + + /// Alias to the output port + PortOut* m_portOut { new PortOut( "to", this ) }; + + /// used only at deserialization + void setData( T& data ); + + public: + static const std::string& getTypename(); +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +template +SingleDataSourceNode::SingleDataSourceNode( const std::string& instanceName, + const std::string& typeName ) : + Node( instanceName, typeName ) { + addOutput( m_portOut, m_data ); +} + +template +bool SingleDataSourceNode::execute() { + // interfaces ports are at the same index as output ports + auto interfacePort = static_cast*>( m_interface[0] ); + if ( interfacePort->isLinked() ) { + // use external storage to deliver data + m_data = &( interfacePort->getData() ); + } + else { + // use local storage to deliver data + m_data = &m_localData; + } + m_portOut->setData( m_data ); + return true; +} + +template +void SingleDataSourceNode::setData( T* data ) { + /// \warning this will copy data into local storage + m_localData = *data; +} + +template +void SingleDataSourceNode::setData( T& data ) { + m_localData = data; +} + +template +T* SingleDataSourceNode::getData() const { + return m_data; +} + +template +void SingleDataSourceNode::setEditable( const std::string& name ) { + Node::addEditableParameter( new EditableParameter( name, m_localData ) ); +} + +template +void SingleDataSourceNode::removeEditable( const std::string& name ) { + Node::removeEditableParameter( name ); +} + +template +const std::string& SingleDataSourceNode::getTypename() { + static std::string demangledTypeName = + std::string { "Source<" } + Ra::Dataflow::Core::simplifiedDemangledType() + ">"; + return demangledTypeName; +} + +template +void SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { + data["comment"] = + std::string( "Unable to save data when serializing a SingleDataSourceNode<" ) + + Ra::Dataflow::Core::simplifiedDemangledType() + ">."; + LOG( Ra::Core::Utils::logDEBUG ) + << "Unable to save data when serializing a " << getTypeName() << "."; +} + +template +bool SingleDataSourceNode::fromJsonInternal( const nlohmann::json& ) { + LOG( Ra::Core::Utils::logDEBUG ) + << "Unable to read data when un-serializing a " << getTypeName() << "."; + return true; +} + +} // namespace Sources +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/Port.hpp b/src/Dataflow/Core/Port.hpp new file mode 100644 index 00000000000..e2b8e4039ef --- /dev/null +++ b/src/Dataflow/Core/Port.hpp @@ -0,0 +1,405 @@ +#pragma once +#include + +#include + +#include +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +class Node; + +/** + * \brief Base class for nodes' ports + * A port is a strongly typed extremity of connections between nodes. + * \warning when comparing and using typed port, beware of the const qualifier + * that is not always exposed by the C++ type system. There are some undefined behavior concerning + * const_casts and const qualifier in the C++ documentation + * (https://en.cppreference.com/w/cpp/language/const_cast). + * + */ +class RA_DATAFLOW_API PortBase +{ + private: + /// The name of the port. + std::string m_name { "" }; + /// The port's data's type's index. + std::type_index m_type; + /// A pointer to the node this port belongs to. + Node* m_node { nullptr }; + + protected: + /// Flag that tells if the port is linked. + bool m_isLinked { false }; + /// Flag that tells if the port must have a connection + bool m_isLinkMandatory { false }; + + public: + /// \name Constructors + /// @{ + /// \brief delete default constructors. + /// \see https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-copy-virtual + /// Constructors. + PortBase() = delete; + PortBase( const PortBase& ) = delete; + PortBase& operator=( const PortBase& ) = delete; + + /// @param name The name of the port. + /// @param type The data's type's hash. + /// @param node The pointer to the node associated with the port. + PortBase( const std::string& name, std::type_index type, Node* node ); + /// @} + + /// \brief make PortBase a base abstract class + virtual ~PortBase() = default; + + /// Gets the port's name. + const std::string& getName(); + /// Gets the type of the data (efficient for comparisons). + std::type_index getType(); + /// Gets a pointer to the node this port belongs to. + Node* getNode(); + virtual bool hasData(); + // TODO : getData() to avoid dynamic_cast to get the data of the PortOut. + /// Returns true if the port is linked + bool isLinked(); + /// Returns true if the port is flagged as being mandatory linked + bool isLinkMandatory(); + /// Flags the port as being mandatory linked + void mustBeLinked(); + virtual PortBase* getLink() = 0; + virtual bool accept( PortBase* other ); + virtual bool connect( PortBase* other ) = 0; + virtual bool disconnect() = 0; + /// Returns a reflected (In <-> Out) port of the same type + virtual PortBase* reflect( Node* node, std::string name ) = 0; + /// Returns true if the port is an input port + virtual bool is_input() { return false; } + + /// Allows to get data stored at this port if it is an output port. + /// This method copy the data onto the given object + /// @params t The reference to store the data of this port + template + void getData( T& t ); + + /// Allows to get data stored at this port if it is an output port. + /// This method do not copy the data but gives a reference to the transmitted object. + /// TODO Verify the robustness of this + /// @params t The reference to store the data of this port + template + T& getData(); + + /// Check if this port is an output port, then takes a pointer to the data this port will point + /// to. + /// @param data The pointer to the data. + template + void setData( T* data ); + + std::string getTypeName(); +}; + +/** + * \brief Output port delivering data of Type T. + * Output port stores a non-owning pointer to the data that will be made available on a connection. + * \tparam T The type of the delivered data. + */ +template +class PortOut : public PortBase +{ + private: + /// The data the port points to. + T* m_data { nullptr }; + + public: + using DataType = T; + /// \name Constructors + /// @{ + /// \brief delete default constructors. + PortOut() = delete; + PortOut( const PortOut& ) = delete; + PortOut& operator=( const PortOut& ) = delete; + /// Constructor. + /// @param name The name of the port. + /// @param node The pointer to the node associated with the port. + PortOut( const std::string& name, Node* node ); + /// @} + + /// Gets a reference to the data this ports points to. + T& getData(); + /// Takes a pointer to the data this port will point to. + /// @param data The pointer to the data. + void setData( T* data ); + /// Returns true if the pointer to the data is not null. + bool hasData() override; + /// Returns nullptr because this port is an out port. + PortBase* getLink() override; + /// Returns false because out ports can not accept connection. + /// @param o The other port to test the connection. + bool accept( PortBase* ) override; + /// Calls the connect(PortBase* o) function of the o node because out ports can not connect. + /// Also sets m_isLinked. + /// @param o The other port to connect. + bool connect( PortBase* o ) override; + /// Returns false because out ports can not disconnect. + bool disconnect() override; + /// Returns a portIn of the same type + PortBase* reflect( Node* node, std::string name ) override; +}; + +/** + * \brief Input port accepting data of type T. + * An input port does not staore the data but is an accessor to the data stored on the connected + * output port. An Input port is observable and notify its observers at each connect/disconnect + * event. \tparam T The accepted data type + */ +template +class PortIn : public PortBase, + public Ra::Core::Utils::Observable&, bool> +{ + private: + /// A pointer to the out port this port is connected to. + PortOut* m_from = nullptr; + + public: + using DataType = T; + /// \name Constructors + /// @{ + /// \brief delete default constructors. + PortIn() = delete; + PortIn( const PortIn& ) = delete; + PortIn& operator=( const PortIn& ) = delete; + /// Constructor. + /// @param name The name of the port. + /// @param node The pointer to the node associated with the port. + PortIn( const std::string& name, Node* node ); + /// @} + + /// Gets the out port this port is connected to. + PortBase* getLink() override; + /// Returns true if the port is linked to an output port that has data. + bool hasData() override; + /// Gets a reference to the data pointed by the connected out port. + /// \note no verification is made about the availability of the data. + T& getData(); + /// Checks if there is not out port already connected and if the data types are the same. + /// @param o The other port to test the connection + bool accept( PortBase* other ) override; + /// Connects this in port and the other out port if there is no out port already connected and + /// if the data types are the same. Also sets m_isLinked. + /// @param o The other port to connect. + bool connect( PortBase* other ) override; + /// Disconnects this port if it is connected. + /// Also sets m_isLinked to false. + bool disconnect() override; + /// Returns a portOut of the same type + PortBase* reflect( Node* node, std::string name ) override; + /// Returns true if the port is an input port + bool is_input() override; +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +inline PortBase::PortBase( const std::string& name, std::type_index type, Node* node ) : + m_name( name ), m_type( type ), m_node( node ) {} + +inline const std::string& PortBase::getName() { + return m_name; +} + +inline std::type_index PortBase::getType() { + return m_type; +} + +inline std::string PortBase::getTypeName() { + return simplifiedDemangledType( m_type ); +} + +inline Node* PortBase::getNode() { + return m_node; +} + +inline bool PortBase::hasData() { + return false; +} + +inline bool PortBase::isLinked() { + return m_isLinked; +} + +inline bool PortBase::isLinkMandatory() { + return m_isLinkMandatory; +} + +inline void PortBase::mustBeLinked() { + m_isLinkMandatory = true; +} + +inline bool PortBase::accept( PortBase* other ) { + return m_type == other->getType(); +} + +template +PortOut::PortOut( const std::string& name, Node* node ) : PortBase( name, typeid( T ), node ) {} + +template +T& PortOut::getData() { + return *m_data; +} + +template +void PortOut::setData( T* data ) { + m_data = data; +} + +template +bool PortOut::hasData() { + return m_data != nullptr; +} + +template +PortBase* PortOut::getLink() { + return nullptr; +} + +template +bool PortOut::accept( PortBase* ) { + return false; +} + +template +bool PortOut::connect( PortBase* o ) { + m_isLinked = o->connect( this ); + return m_isLinked; +} + +template +void PortBase::setData( T* data ) { + if ( !is_input() ) { + auto thisOut = dynamic_cast*>( this ); + if ( thisOut ) { + thisOut->setData( data ); + return; + } + LOG( Ra::Core::Utils::logERROR ) + << "Unable to set data with type " << simplifiedDemangledType( *data ) << " on port " + << getName() << " which expect " << getTypeName() << ".\n"; + std::abort(); + } + LOG( Ra::Core::Utils::logERROR ) + << "Could not call PortBase::setData(T* data) on the input port " << getName() << ".\n"; + std::abort(); +} + +template +void PortBase::getData( T& t ) { + if ( !is_input() ) { + auto thisOut = dynamic_cast*>( this ); + if ( thisOut ) { + t = thisOut->getData(); + return; + } + LOG( Ra::Core::Utils::logERROR ) + << "Unable to get data with type " << simplifiedDemangledType() << " on port " + << getName() << " which expect " << getTypeName() << ".\n"; + std::abort(); + } + LOG( Ra::Core::Utils::logERROR ) + << "Could not call PortBase::getData( T& t ) on the input port " << getName() << ".\n"; + std::abort(); +} + +template +T& PortBase::getData() { + if ( !is_input() ) { + auto thisOut = dynamic_cast*>( this ); + if ( thisOut ) { return thisOut->getData(); } + LOG( Ra::Core::Utils::logERROR ) + << "Unable to get data with type " << simplifiedDemangledType() << " on port " + << getName() << " which expect " << getTypeName() << ".\n"; + std::abort(); + } + LOG( Ra::Core::Utils::logERROR ) + << "Could not call T& PortBase::getData() on the input port " << getName() << ".\n"; + std::abort(); +} + +template +bool PortOut::disconnect() { + return false; +} + +template +PortBase* PortOut::reflect( Node* node, std::string name ) { + return new PortIn( name, node ); +} + +/** + * PortIn is an Observable&> that notifies its observers at connect/disconnect event + * @tparam T + */ +template +PortIn::PortIn( const std::string& name, Node* node ) : PortBase( name, typeid( T ), node ) {} + +template +PortBase* PortIn::getLink() { + return m_from; +} + +template +T& PortIn::getData() { + return m_from->getData(); +} + +template +inline bool PortIn::hasData() { + if ( isLinked() ) { return m_from->hasData(); } + return false; +} + +template +bool PortIn::accept( PortBase* other ) { + if ( !m_from && ( other->getType() == getType() ) ) { return PortBase::accept( other ); } + return false; +} + +template +bool PortIn::connect( PortBase* other ) { + if ( accept( other ) ) { + m_from = static_cast*>( other ); + m_isLinked = true; + // notify after connect + this->notify( getName(), *this, true ); + } + return m_isLinked; +} + +template +bool PortIn::disconnect() { + if ( m_isLinked ) { + // notify before disconnect + this->notify( getName(), *this, false ); + m_from = nullptr; + m_isLinked = false; + return true; + } + return false; +} +template +PortBase* PortIn::reflect( Node* node, std::string name ) { + return new PortOut( name, node ); +} + +template +bool PortIn::is_input() { + return true; +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/TypeDemangler.cpp b/src/Dataflow/Core/TypeDemangler.cpp new file mode 100644 index 00000000000..1f6105d6f17 --- /dev/null +++ b/src/Dataflow/Core/TypeDemangler.cpp @@ -0,0 +1,52 @@ +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +namespace TypeInternal { + +/** \todo verify windows specific type demangling needs. + * + */ +RA_DATAFLOW_API auto makeTypeReadable( const std::string& fullType ) -> std::string { + static std::map knownTypes { + { "std::", "" }, + { "__cxx11::", "" }, + { ", std::allocator", "" }, + { ", std::allocator", "" }, + { ", std::allocator", "" }, + { ", std::allocator", "" }, + { "Ra::Core::VectorArray", "RaVector" }, + { "Ra::Core::Utils::ColorBase", "Color" }, + { "Ra::Core::Utils::ColorBase", "Color" }, + { "Eigen::Matrix", "Vector2" }, + { "Eigen::Matrix", "Vector2d" }, + { "Eigen::Matrix", "Vector2i" }, + { "Eigen::Matrix", "Vector2ui" }, + { "Eigen::Matrix", "Vector3" }, + { "Eigen::Matrix", "Vector3d" }, + { "Eigen::Matrix", "Vector3i" }, + { "Eigen::Matrix", "Vector3ui" }, + { "Eigen::Matrix", "Vector4" }, + { "Eigen::Matrix", "Vector4d" }, + { "Eigen::Matrix", "Vector4i" }, + { "Eigen::Matrix", "Vector4ui" }, + // Windows (visual studio 2022) specific name fix + { " __ptr64", "" } }; + + auto processedType = fullType; + for ( const auto& [key, value] : knownTypes ) { + Ra::Core::Utils::replaceAllInString( processedType, key, value ); + } + return processedType; +} + +} // namespace TypeInternal +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/TypeDemangler.hpp b/src/Dataflow/Core/TypeDemangler.hpp new file mode 100644 index 00000000000..eb337a39d66 --- /dev/null +++ b/src/Dataflow/Core/TypeDemangler.hpp @@ -0,0 +1,53 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Core { + +/// \brief Return the human readable version of the type name T with simplified radium type names +template +auto simplifiedDemangledType() noexcept -> std::string; + +/// \brief Return the human readable version of the type name T with simplified radium type names +template +auto simplifiedDemangledType( const T& ) noexcept -> std::string; + +/// \brief Return the human readable version of the type name whose index is known, with simplified +/// radium type names +/// \param typeName The typeIndex whose simplified named is requested +/// \return The Radium-simplified type name +RA_DATAFLOW_API auto simplifiedDemangledType( const std::type_index& typeName ) noexcept + -> std::string; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +namespace TypeInternal { +RA_DATAFLOW_API auto makeTypeReadable( const std::string& ) -> std::string; +} + +template +auto simplifiedDemangledType() noexcept -> std::string { + static auto demangled_name = []() { + std::string demangledType = + TypeInternal::makeTypeReadable( Ra::Core::Utils::demangleType() ); + return demangledType; + }(); + return demangled_name; +} + +template +auto simplifiedDemangledType( const T& ) noexcept -> std::string { + return simplifiedDemangledType(); +} + +inline auto simplifiedDemangledType( const std::type_index& typeName ) noexcept -> std::string { + return TypeInternal::makeTypeReadable( Ra::Core::Utils::demangleType( typeName ) ); +} + +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Core/filelist.cmake b/src/Dataflow/Core/filelist.cmake new file mode 100644 index 00000000000..7ff6168557c --- /dev/null +++ b/src/Dataflow/Core/filelist.cmake @@ -0,0 +1,36 @@ +# ---------------------------------------------------- +# This file can be generated from a script: +# To do so, run "./generateFilelistForModule.sh Dataflow/Core" +# from ./scripts directory +# ---------------------------------------------------- + +set(dataflow_core_sources DataflowGraph.cpp Node.cpp NodeFactory.cpp Nodes/CoreBuiltInsNodes.cpp + TypeDemangler.cpp +) + +set(dataflow_core_headers + DataflowGraph.hpp + EditableParameter.hpp + Enumerator.hpp + Node.hpp + NodeFactory.hpp + Nodes/CoreBuiltInsNodes.hpp + Nodes/Functionals/BinaryOpNode.hpp + Nodes/Functionals/CoreDataFunctionals.hpp + Nodes/Functionals/FilterNode.hpp + Nodes/Functionals/ReduceNode.hpp + Nodes/Functionals/TransformNode.hpp + Nodes/Sinks/CoreDataSinks.hpp + Nodes/Sinks/SinkNode.hpp + Nodes/Sources/CoreDataSources.hpp + Nodes/Sources/FunctionSource.hpp + Nodes/Sources/SingleDataSourceNode.hpp + Port.hpp + TypeDemangler.hpp +) + +set(dataflow_core_private + Nodes/Private/FunctionalsNodeFactory.hpp Nodes/Private/FunctionalsNodeFactory.cpp + Nodes/Private/SinksNodeFactory.hpp Nodes/Private/SinksNodeFactory.cpp + Nodes/Private/SourcesNodeFactory.hpp Nodes/Private/SourcesNodeFactory.cpp +) diff --git a/src/Dataflow/Core/pch.hpp b/src/Dataflow/Core/pch.hpp new file mode 100644 index 00000000000..6f70f09beec --- /dev/null +++ b/src/Dataflow/Core/pch.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/src/Dataflow/QtGui/CMakeLists.txt b/src/Dataflow/QtGui/CMakeLists.txt new file mode 100644 index 00000000000..c3d2a17472e --- /dev/null +++ b/src/Dataflow/QtGui/CMakeLists.txt @@ -0,0 +1,64 @@ +set(ra_dataflowqtgui_target DataflowQtGui) +list(APPEND CMAKE_MESSAGE_INDENT "[${ra_dataflowqtgui_target}] ") + +project(${ra_dataflowqtgui_target} LANGUAGES CXX VERSION ${Radium_VERSION}) + +find_package(RadiumNodeEditor REQUIRED NO_DEFAULT_PATH) + +include(filelist.cmake) + +# Qt utility functions +include(QtFunctions) + +# Find Qt packages +find_qt_package(COMPONENTS Core Widgets OpenGL Xml REQUIRED) +set(QT_DEFAULT_MAJOR_VERSION ${QT_DEFAULT_MAJOR_VERSION} PARENT_SCOPE) +set(Qt_LIBRARIES Qt::Core Qt::Widgets Qt::OpenGL Qt::Xml) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +qt_wrap_ui(gui_uis ${gui_uis}) + +# configure library +find_package(PowerSlider REQUIRED) + +add_library( + ${ra_dataflowqtgui_target} SHARED ${dataflow_qtgui_sources} ${dataflow_qtgui_headers} + ${dataflow_qtgui_resources} +) + +# This one should be extracted directly from parent project properties. +target_compile_definitions(${ra_dataflowqtgui_target} PRIVATE RA_DATAFLOW_EXPORTS) + +target_compile_options(${ra_dataflowqtgui_target} PUBLIC ${RA_DEFAULT_COMPILE_OPTIONS}) + +target_include_directories(${ra_dataflowqtgui_target} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +add_dependencies(${ra_dataflowqtgui_target} DataflowCore Gui) + +target_link_libraries( + ${ra_dataflowqtgui_target} PUBLIC ${Qt_LIBRARIES} DataflowCore Gui + PUBLIC RadiumNodeEditor::RadiumNodeEditor PRIVATE PowerSlider::PowerSlider +) + +message(STATUS "Configuring library ${ra_dataflowqtgui_target} with standard settings") +configure_radium_target(${ra_dataflowqtgui_target}) +# configure the library only. The package is a sub-package and should be configured independently +configure_radium_library( + TARGET ${ra_dataflowqtgui_target} COMPONENT TARGET_DIR "Dataflow/QtGui" + FILES "${dataflow_qtgui_headers}" +) +# Generate cmake package +configure_radium_package( + NAME ${ra_dataflowqtgui_target} PACKAGE_CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in + PACKAGE_DIR "lib/cmake/Radium/${ra_dataflowqtgui_target}" NAME_PREFIX "Radium" +) + +set(RADIUM_COMPONENTS ${RADIUM_COMPONENTS} ${ra_dataflowqtgui_target} PARENT_SCOPE) + +if(RADIUM_ENABLE_PCH) + target_precompile_headers(${ra_dataflowqtgui_target} PRIVATE pch.hpp) +endif() + +list(REMOVE_AT CMAKE_MESSAGE_INDENT -1) diff --git a/src/Dataflow/QtGui/Config.cmake.in b/src/Dataflow/QtGui/Config.cmake.in new file mode 100644 index 00000000000..42e8dadec3b --- /dev/null +++ b/src/Dataflow/QtGui/Config.cmake.in @@ -0,0 +1,43 @@ +# -------------- Configuration of the Radium DataflowCore targets and definitions ----------------------- +# Setup Engine and check for dependencies + +if (DataflowQtGui_FOUND AND NOT TARGET DataflowQtGui) + set(Configure_DataflowQtGui ON) + # verify dependencies + if(NOT DataflowCore_FOUND) + # if in source dir + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Core/RadiumDataflowCoreConfig.cmake") + set(DataflowCore_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Core/RadiumDataflowCoreConfig.cmake) + else() + # if in install dir + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake") + set(DataflowCore_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::DataflowQtGui: dependency DataflowCore not found") + set(Configure_DataflowQtGui OFF) + endif() + endif() + endif() + if(NOT Gui_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../Gui/RadiumGuiConfig.cmake") + set(Gui_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../Gui/RadiumGuiConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::DataflowQtGui: dependency Gui not found") + set(Configure_DataflowQtGui OFF) + endif() + endif() +endif() + +if(Configure_DataflowQtGui) + set(RadiumNodeEditor_DIR "@RadiumNodeEditor_DIR@") + find_dependency(RadiumNodeEditor REQUIRED NO_DEFAULT_PATH) + if(MSVC OR MSVC_IDE OR MINGW) + add_imported_dir(FROM RadiumNodeEditor::RadiumNodeEditor TO RadiumExternalDlls_location) + endif() + include("${CMAKE_CURRENT_LIST_DIR}/../Dataflow/QtGui/DataflowQtGuiTargets.cmake") +endif() diff --git a/src/Dataflow/QtGui/GraphEditor/ConnectionStatusData.hpp b/src/Dataflow/QtGui/GraphEditor/ConnectionStatusData.hpp new file mode 100644 index 00000000000..ecedafdabac --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/ConnectionStatusData.hpp @@ -0,0 +1,40 @@ +#pragma once +#include + +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { +using namespace Ra::Dataflow::Core; + +/** + * \brief Allow to manage connections in the RadiumNodeEditor external + */ +class RA_DATAFLOW_API ConnectionStatusData : public QtNodes::NodeData +{ + public: + ConnectionStatusData( Node* node, const std::string& outputName ) : + m_node { node }, m_outputName { outputName } {} + + QtNodes::NodeDataType type() const override { + return QtNodes::NodeDataType { "connection", "connectionStatus" }; + } + + Node* getNode() { return m_node; } + std::string getOutputName() { return m_outputName; } + + private: + Node* m_node { nullptr }; + std::string m_outputName; +}; + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/GraphEditor.qrc b/src/Dataflow/QtGui/GraphEditor/GraphEditor.qrc new file mode 100644 index 00000000000..b34fa0ffaa2 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/GraphEditor.qrc @@ -0,0 +1,10 @@ + + + images/copy.png + images/cut.png + images/new.png + images/open.png + images/paste.png + images/save.png + + diff --git a/src/Dataflow/QtGui/GraphEditor/GraphEditorView.cpp b/src/Dataflow/QtGui/GraphEditor/GraphEditorView.cpp new file mode 100644 index 00000000000..34e9aebaca5 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/GraphEditorView.cpp @@ -0,0 +1,165 @@ +#include + +#include + +#include +#include +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +GraphEditorView::GraphEditorView( QWidget* parent ) : QWidget( parent, Qt::Window ) { + QtNodes::ConnectionStyle::setConnectionStyle( + R"( + { + "ConnectionStyle": { + "UseDataDefinedColors": true + } + } + )" ); + + QVBoxLayout* l = new QVBoxLayout( this ); + + scene = new QtNodes::FlowScene( l ); + view = new QtNodes::FlowView( scene ); + + l->addWidget( view ); + l->setContentsMargins( 0, 0, 0, 0 ); + l->setSpacing( 0 ); + + // Create widgets + WidgetFactory::initializeWidgetFactory(); + + connectAll(); +} + +GraphEditorView::~GraphEditorView() { + disconnectAll(); +} + +void GraphEditorView::disconnectAll() { + for ( size_t i = 0; i < connections.size(); i++ ) { + QObject::disconnect( connections[i] ); + } + connections.clear(); +} + +void GraphEditorView::connectAll() { + + // This one is only here to allow modifying node parameters through the embedded widget + // TODO, find another way to do this. + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::nodeHoverLeft, [this]() { emit needUpdate(); } ) ); + + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::nodePlaced, [this]() { emit needUpdate(); } ) ); + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::nodeDeleted, [this]() { emit needUpdate(); } ) ); + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::connectionCreated, [this]() { emit needUpdate(); } ) ); + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::connectionDeleted, [this]() { emit needUpdate(); } ) ); + connections.push_back( QObject::connect( + scene, &QtNodes::FlowScene::nodeMoved, []( QtNodes::Node& n, const QPointF& ) { + QJsonObject obj; + obj["x"] = n.nodeGraphicsObject().pos().x(); + obj["y"] = n.nodeGraphicsObject().pos().y(); + QJsonObject nodeJson; + nodeJson["position"] = obj; + n.nodeDataModel()->addMetaData( nodeJson ); + } ) ); +} + +void GraphEditorView::buildAdapterRegistry( const NodeFactorySet& factories ) { + m_editorRegistry.reset( new QtNodes::DataModelRegistry ); + for ( const auto& [factoryName, factory] : factories ) { + for ( const auto& [typeName, creator] : factory->getFactoryMap() ) { + auto f = creator.first; + auto creatorFactory = factory; + m_editorRegistry->registerModel( + typeName.c_str(), + [f, this, creatorFactory]() -> std::unique_ptr { + nlohmann::json jsondata; + auto node = f( jsondata ); + this->m_dataflowGraph->addNode( std::unique_ptr( node ) ); + this->m_dataflowGraph->addFactory( creatorFactory ); + return std::make_unique( this->m_dataflowGraph, node ); + }, + factoryName.c_str(), + creator.second.c_str() ); + } + } + scene->setRegistry( m_editorRegistry ); +} + +DataflowGraph* GraphEditorView::editedGraph() { + return m_dataflowGraph; +} + +void GraphEditorView::editGraph( DataflowGraph* g ) { + // Disconnect all event and clear the previous graph + disconnectAll(); + scene->clearScene(); + + // Setup the graph to edit + m_dataflowGraph = g; + if ( m_dataflowGraph ) { + buildAdapterRegistry( NodeFactoriesManager::getFactoryManager() ); + const auto& nodes = m_dataflowGraph->getNodes(); + // NodeToUuid mapping + std::map nodeToUuid; + // inserting nodes + for ( const auto& n : nodes ) { + auto nodeAdapter = std::make_unique( m_dataflowGraph, n.get() ); + nodeToUuid.insert( { n.get(), nodeAdapter->uuid() } ); + scene->importNode( std::move( nodeAdapter ) ); + } + // inserting connections + for ( const auto& n : nodes ) { + int numPort = 0; + for ( const auto& input : n->getInputs() ) { + if ( input->isLinked() ) { + auto portOut = input->getLink(); + auto nodeOut = portOut->getNode(); + int outPortIndex = 0; + for ( const auto& p : nodeOut->getOutputs() ) { + if ( p.get() == portOut ) { break; } + outPortIndex++; + } + scene->importConnection( + nodeToUuid[nodeOut], outPortIndex, nodeToUuid[n.get()], numPort ); + } + numPort++; + } + } + scene->setSceneName( m_dataflowGraph->getInstanceName().c_str() ); + } + else { scene->setSceneName( "untitled" ); } + scene->iterateOverNodes( []( QtNodes::Node* n ) { n->onNodeSizeUpdated(); } ); + + // view->fitInView( view->sceneRect(), Qt::KeepAspectRatio); + // view->resetTransform(); + view->ensureVisible( view->sceneRect() ); + view->centerOn( view->sceneRect().center() ); + // Re-connect events + connectAll(); +} + +// Find the way to see all the scene in the editor (or, at leas 75% of the scene) +void GraphEditorView::resizeEvent( QResizeEvent* ) { + // view->resetTransform(); + view->ensureVisible( view->sceneRect() ); + view->centerOn( view->sceneRect().center() ); +} + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/GraphEditorView.hpp b/src/Dataflow/QtGui/GraphEditor/GraphEditorView.hpp new file mode 100644 index 00000000000..203cf19bc72 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/GraphEditorView.hpp @@ -0,0 +1,62 @@ +#pragma once +#include + +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Core { +class DataflowGraph; +class NodeFactorySet; +} // namespace Core + +/** + * + */ +namespace QtGui { +namespace GraphEditor { + +using namespace Ra::Dataflow::Core; + +class RA_DATAFLOW_API GraphEditorView : public QWidget +{ + Q_OBJECT + public: + explicit GraphEditorView( QWidget* parent ); + ~GraphEditorView(); + void disconnectAll(); + void connectAll(); + + /// Fill the editor with the existing nodes in the graph + void editGraph( DataflowGraph* g ); + + DataflowGraph* editedGraph(); + + Q_SIGNALS: + void needUpdate(); + + protected: + void resizeEvent( QResizeEvent* event ) override; + + private: + std::vector connections; + + QtNodes::FlowScene* scene { nullptr }; + QtNodes::FlowView* view { nullptr }; + + /// Build the registries from the graph's NodeFactory + void buildAdapterRegistry( const NodeFactorySet& factory ); + + DataflowGraph* m_dataflowGraph { nullptr }; + + /// Node creator registry to be used when creating a node in the editor + std::shared_ptr m_editorRegistry; +}; + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.cpp b/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.cpp new file mode 100644 index 00000000000..a22d004cebe --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.cpp @@ -0,0 +1,313 @@ +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +using namespace Ra::Dataflow::Core; + +GraphEditorWindow::~GraphEditorWindow() { + if ( m_graph ) { + // Prevent graph destruction if the editor is used to work with active graphs. + auto graphProtection = m_graph->getNodesAndLinksProtection(); + if ( !m_ownGraph ) { m_graph->setNodesAndLinksProtection( true ); } + delete m_graphEdit; + if ( !m_ownGraph ) { m_graph->setNodesAndLinksProtection( graphProtection ); } + else { delete m_graph; } + } + else { delete m_graphEdit; } +} + +GraphEditorWindow::GraphEditorWindow( DataflowGraph* graph ) : + m_graphEdit { new GraphEditorView( nullptr ) }, + m_graph { graph }, + m_ownGraph { graph == nullptr } { + + setCentralWidget( m_graphEdit ); + m_graphEdit->setFocusPolicy( Qt::StrongFocus ); + + createActions(); + createStatusBar(); + + readSettings(); + + connect( + m_graphEdit, &GraphEditorView::needUpdate, this, &GraphEditorWindow::documentWasModified ); + + setCurrentFile( QString() ); + setUnifiedTitleAndToolBarOnMac( true ); + if ( m_ownGraph ) { newFile(); } + else { m_graphEdit->editGraph( m_graph ); } + + m_graphEdit->show(); +} + +#if 0 +void GraphEditorWindow::resetGraph( DataflowGraph* graph ) { + m_graphEdit->editGraph( nullptr ); + if ( m_ownGraph ) { delete m_graph; } + m_graph = graph; + m_ownGraph = false; + m_graphEdit->editGraph( m_graph ); + setCurrentFile( "" ); +} +#endif + +void GraphEditorWindow::closeEvent( QCloseEvent* event ) { + if ( maybeSave() ) { + writeSettings(); + event->accept(); + if ( !m_ownGraph ) { deleteLater(); } + } + else { event->ignore(); } +} + +void GraphEditorWindow::newFile() { + if ( maybeSave() ) { + // Currently edited graph must be deleted only after it is no more used by the editor + m_graphEdit->editGraph( nullptr ); + if ( m_ownGraph ) { + delete m_graph; + m_graph = new DataflowGraph( "untitled.flow" ); + } + else { m_graph->destroy(); } + + setCurrentFile( "" ); + m_graphEdit->editGraph( m_graph ); + } +} + +void GraphEditorWindow::open() { + if ( maybeSave() ) { + QString fileName = QFileDialog::getOpenFileName( this ); + if ( !fileName.isEmpty() ) loadFile( fileName ); + } +} + +bool GraphEditorWindow::save() { + if ( m_curFile.isEmpty() ) { return saveAs(); } + else { return saveFile( m_curFile ); } +} + +bool GraphEditorWindow::saveAs() { + QFileDialog dialog( this ); + dialog.setWindowModality( Qt::WindowModal ); + dialog.setAcceptMode( QFileDialog::AcceptSave ); + dialog.setDefaultSuffix( "json" ); + if ( dialog.exec() != QDialog::Accepted ) return false; + return saveFile( dialog.selectedFiles().first() ); +} + +void GraphEditorWindow::about() { + QMessageBox::about( this, + tr( "About Node Editor" ), + tr( "This is NodeGraph Editor widget from Radium::Dataflow::QtGui." ) ); +} + +void GraphEditorWindow::documentWasModified() { + setWindowModified( m_graph->m_shouldBeSaved ); + emit needUpdate(); +} + +void GraphEditorWindow::createActions() { + + auto fileMenu = menuBar()->addMenu( tr( "&File" ) ); + auto fileToolBar = addToolBar( tr( "File" ) ); + const QIcon newIcon = QIcon::fromTheme( "document-new", QIcon( ":/images/new.png" ) ); + auto newAct = new QAction( newIcon, tr( "&New" ), this ); + newAct->setShortcuts( QKeySequence::New ); + newAct->setStatusTip( tr( "Create a new file" ) ); + connect( newAct, &QAction::triggered, this, &GraphEditorWindow::newFile ); + fileMenu->addAction( newAct ); + fileToolBar->addAction( newAct ); + + const QIcon openIcon = QIcon::fromTheme( "document-open", QIcon( ":/images/open.png" ) ); + auto openAct = new QAction( openIcon, tr( "&Open..." ), this ); + openAct->setShortcuts( QKeySequence::Open ); + openAct->setStatusTip( tr( "Open an existing file" ) ); + connect( openAct, &QAction::triggered, this, &GraphEditorWindow::open ); + fileMenu->addAction( openAct ); + fileToolBar->addAction( openAct ); + + const QIcon saveIcon = QIcon::fromTheme( "document-save", QIcon( ":/images/save.png" ) ); + auto saveAct = new QAction( saveIcon, tr( "&Save" ), this ); + saveAct->setShortcuts( QKeySequence::Save ); + saveAct->setStatusTip( tr( "Save the document to disk" ) ); + connect( saveAct, &QAction::triggered, this, &GraphEditorWindow::save ); + fileMenu->addAction( saveAct ); + fileToolBar->addAction( saveAct ); + + const auto saveAsIcon = QIcon::fromTheme( "document-save-as" ); + auto saveAsAct = + fileMenu->addAction( saveAsIcon, tr( "Save &As..." ), this, &GraphEditorWindow::saveAs ); + saveAsAct->setShortcuts( QKeySequence::SaveAs ); + saveAsAct->setStatusTip( tr( "Save the document under a new name" ) ); + + fileMenu->addSeparator(); + + const QIcon exitIcon = QIcon::fromTheme( "application-exit" ); + auto exitAct = fileMenu->addAction( exitIcon, tr( "E&xit" ), this, &QWidget::close ); + exitAct->setShortcuts( QKeySequence::Quit ); + exitAct->setStatusTip( tr( "Exit the application" ) ); + +#if 0 + // Activite this section when editMenu might be filled + auto editMenu = menuBar()->addMenu( tr( "&Edit" ) ); + auto editToolBar = addToolBar( tr( "Edit" ) ); +#endif + + auto helpMenu = menuBar()->addMenu( tr( "&Help" ) ); + auto aboutAct = helpMenu->addAction( tr( "&About" ), this, &GraphEditorWindow::about ); + aboutAct->setStatusTip( tr( "Show the application's About box" ) ); + + auto aboutQtAct = helpMenu->addAction( tr( "About &Qt" ), qApp, &QApplication::aboutQt ); + aboutQtAct->setStatusTip( tr( "Show the Qt library's About box" ) ); + if ( !m_ownGraph ) { menuBar()->hide(); } +} + +void GraphEditorWindow::createStatusBar() { + statusBar()->showMessage( tr( "Ready" ) ); +} + +void GraphEditorWindow::readSettings() { + QSettings settings( QCoreApplication::organizationName(), QCoreApplication::applicationName() ); + settings.beginGroup( "nodegraph editor" ); + const QByteArray geometry = settings.value( "geometry", QByteArray() ).toByteArray(); + if ( geometry.isEmpty() ) { + const QRect availableGeometry = screen()->availableGeometry(); + resize( availableGeometry.width() / 3, availableGeometry.height() / 2 ); + move( ( availableGeometry.width() - width() ) / 2, + ( availableGeometry.height() - height() ) / 2 ); + } + else { restoreGeometry( geometry ); } + const QByteArray graphGeometry = settings.value( "graph", QByteArray() ).toByteArray(); + if ( graphGeometry.isEmpty() ) { m_graphEdit->resize( 800, 600 ); } + else { m_graphEdit->restoreGeometry( graphGeometry ); } + settings.endGroup(); +} + +void GraphEditorWindow::writeSettings() { + QSettings settings( QCoreApplication::organizationName(), QCoreApplication::applicationName() ); + settings.beginGroup( "nodegraph editor" ); + settings.setValue( "geometry", saveGeometry() ); + settings.setValue( "graph", m_graphEdit->saveGeometry() ); + settings.endGroup(); +} + +bool GraphEditorWindow::maybeSave() { + if ( m_graph == nullptr ) { return true; } + if ( !( m_ownGraph && m_graph->m_shouldBeSaved ) ) { return true; } + const QMessageBox::StandardButton ret = + QMessageBox::warning( this, + tr( "Application" ), + tr( "The document has been modified.\n" + "Do you want to save your changes?" ), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel ); + switch ( ret ) { + case QMessageBox::Save: + return save(); + case QMessageBox::Cancel: + return false; + default: + break; + } + + return true; +} + +void GraphEditorWindow::loadFile( const QString& fileName ) { + QGuiApplication::setOverrideCursor( Qt::WaitCursor ); + { + QFile file( fileName ); + if ( !file.open( QFile::ReadOnly | QFile::Text ) ) { + QMessageBox::warning( + this, + tr( "Application" ), + tr( "Cannot read file %1:\n%2." ) + .arg( QDir::toNativeSeparators( fileName ), file.errorString() ) ); + return; + } + } + + bool loaded( true ); + if ( m_ownGraph ) { + m_graphEdit->editGraph( nullptr ); + delete m_graph; + m_graph = DataflowGraph::loadGraphFromJsonFile( fileName.toStdString() ); + loaded = ( m_graph != nullptr ); + } + else { + m_graphEdit->editGraph( nullptr ); + m_graph->destroy(); + loaded = m_graph->loadFromJson( fileName.toStdString() ); + } + if ( !loaded ) { + QMessageBox::warning( + this, + tr( "Application" ), + tr( "Can't load graph from file %1.\n" ).arg( QDir::toNativeSeparators( fileName ) ) ); + } + + QGuiApplication::restoreOverrideCursor(); + if ( loaded ) { + m_graphEdit->editGraph( m_graph ); + setCurrentFile( fileName ); + statusBar()->showMessage( tr( "File loaded" ), 2000 ); + emit needUpdate(); + } +} + +bool GraphEditorWindow::saveFile( const QString& fileName ) { + QString errorMessage; + + QGuiApplication::setOverrideCursor( Qt::WaitCursor ); + // TODO, if graph do not compile, tell it to the user ? + // m_graph->compile(); + + m_graph->saveToJson( fileName.toStdString() ); +#if 0 + QSaveFile file(fileName); + + if (file.open(QFile::WriteOnly | QFile::Text)) { + QTextStream out(&file); + out << textEdit->toPlainText(); + if (!file.commit()) { + errorMessage = tr("Cannot write file %1:\n%2.") + .arg(QDir::toNativeSeparators(fileName), file.errorString()); + } + } else { + errorMessage = tr("Cannot open file %1 for writing:\n%2.") + .arg(QDir::toNativeSeparators(fileName), file.errorString()); + } +#endif + QGuiApplication::restoreOverrideCursor(); + + if ( !errorMessage.isEmpty() ) { + QMessageBox::warning( this, tr( "Application" ), errorMessage ); + return false; + } + + setCurrentFile( fileName ); + statusBar()->showMessage( tr( "File saved" ), 2000 ); + return true; +} + +void GraphEditorWindow::setCurrentFile( const QString& fileName ) { + m_curFile = fileName; + // textEdit->document()->setModified(false); + setWindowModified( false ); + + QString shownName = m_curFile; + if ( m_curFile.isEmpty() ) shownName = "untitled.flow"; + setWindowFilePath( shownName ); +} + +QString GraphEditorWindow::strippedName( const QString& fullFileName ) { + return QFileInfo( fullFileName ).fileName(); +} + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.hpp b/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.hpp new file mode 100644 index 00000000000..152f87b2d10 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/GraphEditorWindow.hpp @@ -0,0 +1,74 @@ +#pragma once +#include + +#include +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +using namespace Ra::Dataflow::Core; + +/** + * \brief Window widget to edit a Node Graph. + * This class just wrap a Ra::Dataflow::QtGui::GraphEditor::GraphEditorView in a main window with + * several services : + * - The window can be used as a standalone editor if no parameter is given to the constructor. + * - The window can be used to edit an existing graph by giving the graph to the constructor. + * + * connect the client to the needUpdate() signal to be notified of a change in the edited graph. + */ +class RA_DATAFLOW_API GraphEditorWindow : public QMainWindow +{ + Q_OBJECT + public: + explicit GraphEditorWindow( DataflowGraph* graph = nullptr ); + ~GraphEditorWindow(); + + void loadFile( const QString& fileName ); +#if 0 + // not sure to neeed this + void resetGraph( DataflowGraph* graph = nullptr ); +#endif + + signals: + void needUpdate(); + + protected: + void closeEvent( QCloseEvent* event ) override; + + private slots: + void newFile(); + void open(); + bool save(); + bool saveAs(); + void about(); + void documentWasModified(); + + private: + void createActions(); + void createStatusBar(); + void readSettings(); + void writeSettings(); + bool maybeSave(); + bool saveFile( const QString& fileName ); + void setCurrentFile( const QString& fileName ); + QString strippedName( const QString& fullFileName ); + + GraphEditorView* m_graphEdit { nullptr }; + QString m_curFile; + + DataflowGraph* m_graph { nullptr }; + bool m_ownGraph { false }; +}; + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.cpp b/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.cpp new file mode 100644 index 00000000000..7ceb27eb1b4 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.cpp @@ -0,0 +1,336 @@ +#include + +#include + +#include + +#include +#include +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +namespace NodeDataModelTools { +/** + * Construct the widget needed to edit all the EditableParameter of the given node + * @param node The node to decorate + * @return the widget (of type RadiumAddons::Gui::Widgets::ControlPanel) exposing the parameters + */ +QWidget* getWidget( Node* node ); + +/** + * Update the state of the widget to reflect the ones of the node. + * @param node + * @param widget + */ +void updateWidget( Node* node, QWidget* widget ); + +/** + * Convert a QJsonObject to a nlohmann::json + */ +void QJsonObjectToNlohmannObject( const QJsonObject& p, nlohmann::json& data ); + +/** + * Convert a nlohmann::json to a QJsonObject + */ +void NlohmannObjectToQJsonObject( const nlohmann::json& data, QJsonObject& p ); +} // namespace NodeDataModelTools + +using namespace Ra::Dataflow::Core; +using namespace Ra::Gui::Widgets; + +NodeAdapterModel::NodeAdapterModel( DataflowGraph* graph, Node* n ) : + m_node { n }, m_dataflowGraph { graph } { + m_uuid = QUuid::createUuid(); + m_inputsConnected.resize( m_node->getInputs().size() ); + m_widget = NodeDataModelTools::getWidget( m_node ); + NodeDataModelTools::updateWidget( m_node, m_widget ); + checkConnections(); +} + +void NodeAdapterModel::checkConnections() const { + int errors = 0; + for ( size_t i = 0; i < m_inputsConnected.size(); i++ ) { + if ( !m_inputsConnected[i] && m_node->getInputs()[i]->isLinkMandatory() ) { errors++; } + } + + if ( errors == 0 ) { + m_validationState = QtNodes::NodeValidationState::Valid; + m_validationError = QString( "" ); + } + else { + if ( errors > 1 ) { + m_validationError = QString( + std::string( std::to_string( errors ) + " mandatory ports are not linked (*)." ) + .c_str() ); + } + else { m_validationError = "1 mandatory port is not linked (*)."; } + + m_validationState = QtNodes::NodeValidationState::Error; + } +} + +unsigned int NodeAdapterModel::nPorts( QtNodes::PortType portType ) const { + unsigned int result = 1; + + switch ( portType ) { + case QtNodes::PortType::In: { + result = m_node->getInputs().size(); + break; + } + + case QtNodes::PortType::Out: { + result = m_node->getOutputs().size(); + break; + } + + default: { + break; + } + } + + return result; +} + +QtNodes::NodeDataType NodeAdapterModel::dataType( QtNodes::PortType portType, + QtNodes::PortIndex portIndex ) const { + QtNodes::NodeDataType result { "incorrect", "incorrect" }; + + switch ( portType ) { + case QtNodes::PortType::In: { + std::string mandatory = ( m_node->getInputs()[portIndex]->isLinkMandatory() ) ? "*" : ""; + return IOToDataType( m_node->getInputs()[portIndex]->getTypeName(), + m_node->getInputs()[portIndex]->getName() + mandatory ); + } + + case QtNodes::PortType::Out: { + return IOToDataType( m_node->getOutputs()[portIndex]->getTypeName(), + m_node->getOutputs()[portIndex]->getName() ); + } + default: + break; + } + + checkConnections(); + + return result; +} + +std::shared_ptr NodeAdapterModel::outData( QtNodes::PortIndex port ) { + return std::make_shared( m_node, m_node->getOutputs()[port]->getName() ); +} + +void NodeAdapterModel::setInData( std::shared_ptr data, int port ) { + auto connectionData = dynamic_cast( data.get() ); + if ( connectionData ) { + // Add connection + m_dataflowGraph->addLink( connectionData->getNode(), + connectionData->getOutputName(), + m_node, + m_node->getInputs()[port]->getName() ); + m_inputsConnected[port] = true; + } + else { + // Remove connection + m_dataflowGraph->removeLink( m_node, m_node->getInputs()[port]->getName() ); + m_inputsConnected[port] = false; + } + checkConnections(); +} + +QtNodes::NodeDataType NodeAdapterModel::IOToDataType( const std::string& typeName, + const std::string& ioName ) const { + return QtNodes::NodeDataType { typeName.c_str(), ioName.c_str() }; +} + +void NodeAdapterModel::updateState() { + int i = 0; + for ( const auto& in : m_node->getInputs() ) { + if ( in->isLinked() ) { m_inputsConnected[i] = true; } + i++; + } + checkConnections(); +} + +void NodeAdapterModel::addMetaData( QJsonObject& json ) { + nlohmann::json data; + NodeDataModelTools::QJsonObjectToNlohmannObject( json, data ); + m_node->addJsonMetaData( data ); +} + +NodeAdapterModel::~NodeAdapterModel() { + m_dataflowGraph->removeNode( m_node ); +} + +QJsonObject NodeAdapterModel::save() const { + QJsonObject o; // = QtNodes::NodeDataModel::save(); + nlohmann::json nodeData; + m_node->toJson( nodeData ); + NodeDataModelTools::NlohmannObjectToQJsonObject( nodeData, o ); + return o; +} + +void NodeAdapterModel::restore( QJsonObject const& p ) { + // QtNodes::NodeDataModel::restore( p ); + // 1 - convert the QJsonObject to nlohmann::json + nlohmann::json nodeData; + NodeDataModelTools::QJsonObjectToNlohmannObject( p, nodeData ); + // 2 - call fromjson on the node using this json object + m_node->fromJson( nodeData ); + // 3 - update the widget according to the editable parameters + NodeDataModelTools::updateWidget( m_node, m_widget ); +} + +namespace NodeDataModelTools { + +QWidget* getWidget( Node* node ) { + if ( node->getEditableParameters().size() == 0 ) { return nullptr; } + + auto controlPanel = new ControlPanel( "Editable parameters", false, nullptr ); + controlPanel->beginLayout( QBoxLayout::TopToBottom ); + for ( size_t i = 0; i < node->getEditableParameters().size(); i++ ) { + auto edtParam = node->getEditableParameters()[i].get(); + QWidget* newWidget = WidgetFactory::createWidget( edtParam ); + if ( newWidget ) { + newWidget->setParent( controlPanel ); + newWidget->setObjectName( edtParam->getName().c_str() ); + controlPanel->addLabel( edtParam->getName() ); + controlPanel->addWidget( newWidget ); + + if ( i != node->getEditableParameters().size() - 1 ) { controlPanel->addSeparator(); } + } + } + controlPanel->endLayout(); + controlPanel->setVisible( true ); + + return controlPanel; +} + +void updateWidget( Node* node, QWidget* widget ) { + if ( node->getEditableParameters().size() == 0 ) { return; } + for ( size_t i = 0; i < node->getEditableParameters().size(); i++ ) { + auto edtParam = node->getEditableParameters()[i].get(); + if ( !WidgetFactory::updateWidget( widget, edtParam ) ) { + LOG( Ra::Core::Utils::logWARNING ) + << "NodeAdapterModel : unable to update parameter " << edtParam->getName() + << " on node " << node->getInstanceName() << " (" << node->getTypeName() << ")"; + } + } +} + +/** Convert from Qt::Json to nlohmann::json */ +void QJsonEntryToNlohmannEntry( const QString& key, + const QJsonValue& value, + nlohmann::json& data ) { + switch ( value.type() ) { + case QJsonValue::Bool: + data[key.toStdString()] = value.toBool(); + break; + case QJsonValue::Double: + // as there is no QJsonValue::Int, manage explicitely keys with int value :( + // TODO find a better way to do that ... + // type is a specific entry of envmapdatasource + if ( key.compare( "type" ) == 0 ) { data[key.toStdString()] = int( value.toDouble() ); } + else { data[key.toStdString()] = Scalar( value.toDouble() ); } + + break; + case QJsonValue::String: + data[key.toStdString()] = value.toString().toStdString(); + break; + case QJsonValue::Array: { + auto jsArray = value.toArray(); + switch ( jsArray.first().type() ) { + case QJsonValue::Double: { + std::vector array; + for ( auto v : jsArray ) { + array.push_back( Scalar( v.toDouble() ) ); + } + data[key.toStdString()] = array; + break; + } + default: + LOG( Ra::Core::Utils::logERROR ) + << "Only Scalar arrays are supported for Json conversion."; + } + } break; + default: + LOG( Ra::Core::Utils::logERROR ) << "QJson to nlohmann::json : QtJson value type " + << value.type() << " is not suported."; + } +} + +void NlohmannEntryToQJsonEntry( const nlohmann::json& data, QJsonValue& value ) { + switch ( data.type() ) { + case nlohmann::detail::value_t::boolean: { + auto v = data.get(); + value = v; + } break; + case nlohmann::detail::value_t::number_float: { + auto v = double( data.get() ); + value = v; + } break; + case nlohmann::detail::value_t::number_integer: { + auto v = data.get(); + value = v; + } break; + case nlohmann::detail::value_t::number_unsigned: { + auto v = data.get(); + value = int( v ); + } break; + case nlohmann::detail::value_t::string: { + auto v = data.get(); + value = v.c_str(); + } break; + case nlohmann::detail::value_t::array: { + QJsonArray a; + QJsonValue v; + for ( auto& x : data.items() ) { + NlohmannEntryToQJsonEntry( x.value(), v ); + a.append( v ); + } + value = a; + } break; + default: + LOG( Ra::Core::Utils::logERROR ) << "nlohmann::json to QJson : nlohmann json type " + << int( data.type() ) << " is not supported."; + } +} + +void QJsonObjectToNlohmannObject( const QJsonObject& p, nlohmann::json& data ) { + for ( const auto& key : p.keys() ) { + auto value = p.value( key ); + if ( value.isObject() ) { + nlohmann::json j; + QJsonObjectToNlohmannObject( value.toObject(), j ); + data[key.toStdString()] = j; + } + else { QJsonEntryToNlohmannEntry( key, value, data ); } + } +} + +void NlohmannObjectToQJsonObject( const nlohmann::json& data, QJsonObject& p ) { + for ( const auto& [key, value] : data.items() ) { + if ( value.is_object() ) { + QJsonObject o; + NlohmannObjectToQJsonObject( data[key], o ); + p.insert( key.c_str(), o ); + } + else { + QJsonValue v; + NlohmannEntryToQJsonEntry( value, v ); + p.insert( key.c_str(), v ); + } + } +} + +} // namespace NodeDataModelTools + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.hpp b/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.hpp new file mode 100644 index 00000000000..d21d0781151 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/NodeAdapterModel.hpp @@ -0,0 +1,87 @@ +#pragma once +#include + +#include + +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +using namespace Ra::Dataflow::Core; + +class RA_DATAFLOW_API NodeAdapterModel : public QtNodes::NodeDataModel +{ + public: + NodeAdapterModel( DataflowGraph* graph, Node* n ); + NodeAdapterModel() = delete; + NodeAdapterModel( const NodeAdapterModel& ) = delete; + NodeAdapterModel( NodeAdapterModel&& ) = delete; + NodeAdapterModel& operator=( const NodeAdapterModel& ) = delete; + NodeAdapterModel& operator=( NodeAdapterModel&& ) = delete; + ~NodeAdapterModel() override; + + public: + QString caption() const override { return m_node->getTypeName().c_str(); } + + bool captionVisible() const override { return true; } + + QString name() const override { return m_node->getTypeName().c_str(); } + + QString uuid() const override { return m_uuid.toString(); } + + bool isDeletable() override { return true; } // Assume all nodes belong to the graph + + void updateState() override; + + void addMetaData( QJsonObject& json ) override; + + private: + QtNodes::NodeDataType IOToDataType( const std::string& typeName, + const std::string& ioName ) const; + + void checkConnections() const; + + public: + unsigned int nPorts( QtNodes::PortType portType ) const override; + + QtNodes::NodeDataType dataType( QtNodes::PortType portType, + QtNodes::PortIndex portIndex ) const override; + + std::shared_ptr outData( QtNodes::PortIndex port ) override; + + void setInData( std::shared_ptr data, int port ) override; + + QtNodes::NodeValidationState validationState() const override { return m_validationState; } + + QString validationMessage() const override { return m_validationError; } + + QWidget* embeddedWidget() override { return m_widget; } + + private: + Node* m_node; + DataflowGraph* m_dataflowGraph { nullptr }; + + QWidget* m_widget { nullptr }; + + std::vector m_inputsConnected; + mutable QtNodes::NodeValidationState m_validationState = QtNodes::NodeValidationState::Valid; + mutable QString m_validationError = QString( "" ); + + QUuid m_uuid; + + public: + QJsonObject save() const override; + void restore( QJsonObject const& p ) override; +}; + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/WidgetFactory.cpp b/src/Dataflow/QtGui/GraphEditor/WidgetFactory.cpp new file mode 100644 index 00000000000..ed089cfcb2b --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/WidgetFactory.cpp @@ -0,0 +1,374 @@ +#include + +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { + +using namespace Ra::Dataflow::Core; +using namespace Ra::Gui::Widgets; + +using namespace Ra::Engine::Data; + +namespace WidgetFactory { +using WidgetFunctionPair = std::pair; + +std::unordered_map widgetsfunctions; + +void registerWidgetInternal( std::type_index typeIdx, + WidgetCreatorFunc widgetCreator, + WidgetUpdaterFunc widgetUpdater ) { + if ( widgetsfunctions.find( typeIdx ) == widgetsfunctions.end() ) { + widgetsfunctions[typeIdx] = { std::move( widgetCreator ), std::move( widgetUpdater ) }; + } + else { + LOG( Ra::Core::Utils::logWARNING ) + << "WidgetFactory: trying to add an already existing widget builder for type " + << simplifiedDemangledType( typeIdx ) << "."; + } +} + +QWidget* createWidget( EditableParameterBase* editableParameter ) { + if ( widgetsfunctions.find( editableParameter->getType() ) != widgetsfunctions.end() ) { + return widgetsfunctions[editableParameter->getType()].first( editableParameter ); + } + else { + // TODO, when PR #1027 will be merged, demangle the type name + LOG( Ra::Core::Utils::logWARNING ) + << "WidgetFactory : no defined widget builder for type " + << simplifiedDemangledType( editableParameter->getType() ) << "."; + } + return nullptr; +} + +bool updateWidget( QWidget* widget, EditableParameterBase* editableParameter ) { + if ( widgetsfunctions.find( editableParameter->getType() ) != widgetsfunctions.end() ) { + return widgetsfunctions[editableParameter->getType()].second( widget, editableParameter ); + } + else { + LOG( Ra::Core::Utils::logWARNING ) + << "WidgetFactory: no defined widget updater for type " + << simplifiedDemangledType( editableParameter->getType() ) << "."; + } + return false; +} + +void initializeWidgetFactory() { + // TODO, rewrite this to use Control panel and its widgets !!! + /* + * Environment map "edition" widget + */ + WidgetFactory::registerWidget>( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast>*>( + editableParameter ); + + auto controlPanel = new ControlPanel( editable->getName(), false ); + auto envmpClbck = [editable, controlPanel]( const std::string& files ) { + if ( files.empty() ) { editable->m_data = nullptr; } + else { + // for now, only skyboxes are managed + editable->m_data = std::make_shared( files, true ); + auto slider = controlPanel->findChild( "strength" ); + if ( slider ) { editable->m_data->setStrength( slider->value() / 100. ); } + } + }; + // TODO : display the name of the envmap image somewhere + controlPanel->addFileInput( + "files", envmpClbck, "../", "Images (*.png *.jpg *.pfm *.exr *hdr)" ); + + auto strengthClbk = [editable]( double v ) { + if ( editable->m_data ) { + auto* env = editable->m_data.get(); + if ( env ) { env->setStrength( v / 100. ); } + } + }; + float s_init = 100.; + if ( editable->m_data ) { s_init = editable->m_data->getStrength() * 100.; } + controlPanel->addPowerSliderInput( "strength", strengthClbk, s_init, 0., 100 ); + controlPanel->setVisible( true ); + return controlPanel; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast>*>( + editableParameter ); + auto slider = widget->findChild( "strength" ); + if ( slider && editable->m_data ) { + slider->setValue( editable->m_data->getStrength() * 100. ); + return true; + } + else { return slider != nullptr; } + } ); + + /* + * Scalar edition + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast*>( editableParameter ); + auto powerSlider = new PowerSlider(); + powerSlider->setObjectName( editable->getName().c_str() ); + // editable->m_data = 0.0_ra; + powerSlider->setValue( editable->m_data ); + const auto& constraints = editable->getConstraints(); + Scalar minValue = 0_ra; + Scalar maxValue = 1000_ra; + if ( constraints.contains( "min" ) ) { minValue = Scalar( constraints["min"] ); } + if ( constraints.contains( "max" ) ) { maxValue = Scalar( constraints["max"] ); } + powerSlider->setRange( minValue, maxValue ); + PowerSlider::connect( powerSlider, + &PowerSlider::valueChanged, + [editable]( Scalar value ) { editable->m_data = value; } ); + return powerSlider; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast*>( editableParameter ); + auto slider = widget->findChild( editableParameter->getName().c_str() ); + if ( slider ) { + slider->setValue( editable->m_data ); + return true; + } + else { return false; } + } ); + /* + * int edition + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast*>( editableParameter ); + const auto& constraints = editable->getConstraints(); + Scalar minValue = 0_ra; + Scalar maxValue = 1000_ra; + if ( constraints.contains( "min" ) ) { minValue = Scalar( constraints["min"] ); } + if ( constraints.contains( "max" ) ) { maxValue = Scalar( constraints["max"] ); } + + auto powerSlider = new PowerSlider(); + powerSlider->setObjectName( editable->getName().c_str() ); + // editable->m_data = 0.0_ra; + powerSlider->setValue( editable->m_data ); + powerSlider->setRange( minValue, maxValue ); + powerSlider->setSingleStep( 1 ); + PowerSlider::connect( powerSlider, + &PowerSlider::valueChanged, + [editable]( Scalar value ) { editable->m_data = int( value ); } ); + return powerSlider; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast*>( editableParameter ); + auto slider = widget->findChild( editableParameter->getName().c_str() ); + if ( slider ) { + slider->setValue( editable->m_data ); + return true; + } + else { return false; } + } ); + /* + * Boolean edition + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast*>( editableParameter ); + auto checkBox = new QCheckBox(); + checkBox->setObjectName( editable->getName().c_str() ); + checkBox->setCheckState( editable->m_data ? Qt::CheckState::Checked + : Qt::CheckState::Unchecked ); + QCheckBox::connect( checkBox, &QCheckBox::stateChanged, [editable]( int state ) { + if ( state == Qt::Unchecked ) { editable->m_data = false; } + else if ( state == Qt::Checked ) { editable->m_data = true; } + } ); + return checkBox; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast*>( editableParameter ); + auto checkBox = widget->findChild( editableParameter->getName().c_str() ); + if ( checkBox ) { + checkBox->setCheckState( editable->m_data ? Qt::Checked : Qt::Unchecked ); + return true; + } + else { return false; } + } ); + + /* + * Color edition + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = + dynamic_cast*>( editableParameter ); + auto controlPanel = new ControlPanel( editable->getName(), false ); + auto clrCbk = [editable]( const Ra::Core::Utils::Color& clr ) { + editable->m_data = clr; + }; + controlPanel->addColorInput( editable->getName(), clrCbk, editable->m_data, true ); + controlPanel->setVisible( true ); + return controlPanel; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = + dynamic_cast*>( editableParameter ); + auto button = widget->findChild( editable->getName().c_str() ); + if ( button ) { + // todo, update the color on the button and make the dialog to take its + // initial color from the button. Once dont, return true ... + auto srgbColor = Ra::Core::Utils::Color::linearRGBTosRGB( editable->m_data ); + auto clrBttn = + QColor::fromRgbF( srgbColor[0], srgbColor[1], srgbColor[2], srgbColor[3] ); + + auto lum = 0.2126_ra * Scalar( clrBttn.redF() ) + + 0.7151_ra * Scalar( clrBttn.greenF() ) + + 0.0721_ra * Scalar( clrBttn.blueF() ); + QString qss = QString( "background-color: %1" ).arg( clrBttn.name() ); + if ( lum > 1_ra / 3_ra ) { qss += QString( "; color: #000000" ); } + else { qss += QString( "; color: #FFFFFF" ); } + button->setStyleSheet( qss ); + return true; + } + LOG( Ra::Core::Utils::logWARNING ) + << " Unable to find the button \"Choose color\" for \"" << editable->getName() + << "\" "; + return false; + } ); + + /* + * Button edition + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast*>( editableParameter ); + auto button = new QPushButton( "Show graph" ); + button->setObjectName( editableParameter->getName().c_str() ); + QPushButton::connect( button, &QPushButton::clicked, [editable]() { + // Display a window to see the graph + /*auto graph = reinterpret_cast( editable ); + auto nodeEditor = new GraphEditorView( nullptr ); + + nodeEditor->editGraph( nullptr ); + nodeEditor->editGraph( graph ); + + nodeEditor->resize( 900, 500 ); + nodeEditor->move( 450, 260 ); + nodeEditor->show(); */ + } ); + + return button; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast*>( editableParameter ); + auto button = widget->findChild( editable->getName().c_str() ); + if ( button ) { return true; } + else { return false; } + } ); + +#if HAS_TRANSFER_FUNCTION + /* + * Transfer function + */ + WidgetFactory::registerWidget>( + []( EditableParameterBase* editableParameter ) { + auto editableTransferFunction = + dynamic_cast>*>( editableParameter ); + // TODO, give a name to the widget so that they can be updated automatically when + // loading a node + auto button = new QPushButton( "Open widget" ); + auto transferEditor = new TransferEditor(); + TransferEditor::connect( + transferEditor, + &TransferEditor::propertiesChanged, + [editableTransferFunction, transferEditor]() { + int pos = 0; + for ( int i = 0; i < 256; i++ ) { + unsigned int color = transferEditor->colorAt( i ); + editableTransferFunction->m_data.at( pos ) = + (unsigned char)( ( 0x00ff0000 & color ) >> 16 ) / 255.f; + editableTransferFunction->m_data.at( pos + 1 ) = + (unsigned char)( ( 0x0000ff00 & color ) >> 8 ) / 255.f; + editableTransferFunction->m_data.at( pos + 2 ) = + (unsigned char)( ( 0x000000ff & color ) ) / 255.f; + editableTransferFunction->m_data.at( pos + 3 ) = + (unsigned char)( ( 0xff000000 & color ) >> 24 ) / 255.f; + pos = pos + 4; + } + } ); + QPushButton::connect( + button, &QPushButton::clicked, [transferEditor]() { transferEditor->show(); } ); + return button; + }, + []( QWidget*, EditableParameterBase* ) -> bool { return false; } ); +#endif + /* + * String enumerator + */ + WidgetFactory::registerWidget>( + []( EditableParameterBase* editableParameter ) { + auto editable = + dynamic_cast>*>( editableParameter ); + auto selector = new QComboBox(); + selector->setObjectName( editable->getName().c_str() ); + for ( const auto& e : editable->m_data ) { + selector->addItem( e.c_str() ); + } + QComboBox::connect( + selector, &QComboBox::currentTextChanged, [editable]( const QString& string ) { + editable->m_data.set( string.toStdString() ); + } ); + return selector; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = + dynamic_cast>*>( editableParameter ); + auto comboBox = widget->findChild( editable->getName().c_str() ); + if ( comboBox ) { + comboBox->setCurrentText( editable->m_data.get().c_str() ); + return true; + } + else { return false; } + } ); + + /* + * String + */ + WidgetFactory::registerWidget( + []( EditableParameterBase* editableParameter ) { + auto editable = dynamic_cast*>( editableParameter ); + auto line = new QLineEdit(); + line->setObjectName( editable->getName().c_str() ); + QLineEdit::connect( line, &QLineEdit::textEdited, [editable]( const QString& string ) { + editable->m_data = string.toStdString(); + } ); + return line; + }, + []( QWidget* widget, EditableParameterBase* editableParameter ) -> bool { + auto editable = dynamic_cast*>( editableParameter ); + auto line = widget->findChild( editable->getName().c_str() ); + if ( line ) { + line->setText( editable->m_data.c_str() ); + return true; + } + else { return false; } + } ); + /* + * Ra::Engine::Data::ShaderConfiguration --> Code Editor + */ +} + +} // namespace WidgetFactory + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/WidgetFactory.hpp b/src/Dataflow/QtGui/GraphEditor/WidgetFactory.hpp new file mode 100644 index 00000000000..e6ad3d52668 --- /dev/null +++ b/src/Dataflow/QtGui/GraphEditor/WidgetFactory.hpp @@ -0,0 +1,82 @@ +#pragma once +#include + +#include + +#include + +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace QtGui { +namespace GraphEditor { +using namespace Ra::Dataflow::Core; + +// TODO : instead of a namespace, make it a class ? +/** + * Set of functions to manage automatic widget creation and update for editable parameters in the Qt + * Node Editor. + * \todo use Ra::Gui::ParameterSetEditor ? + */ +namespace WidgetFactory { +/** Type of the function that creates a widget. + * @param the editable parameter that defines the widget content + * @return the created widget + */ +using WidgetCreatorFunc = std::function; +/** Type of the function that creates a widget. + * @param the widget whose content must be updated + * @param the editable parameter that defines the widget content + * @return true if update is done, false if not + */ +using WidgetUpdaterFunc = std::function; + +/** private method to manage the factory + */ +RA_DATAFLOW_API void registerWidgetInternal( std::type_index typeIdx, + WidgetCreatorFunc widgetCreator, + WidgetUpdaterFunc widgetUpdater ); + +/** Register a widget builder and updater in the factory given the type of the editable parameter + * @tparam T The concrete type of the editable parameter + * @param widgetCreator a function that build a widget to edit the given parameter. + * @param widgetUpdater a function to update the widget state according to the given parameter. + */ +template +void registerWidget( WidgetCreatorFunc widgetCreator, WidgetUpdaterFunc widgetUpdater ) { + registerWidgetInternal( typeid( T ), std::move( widgetCreator ), std::move( widgetUpdater ) ); +} + +/** + * Create a widget from an editable parameter using the widget factory + * @param editableParameter the data whose type will define the widget + * @return the created widget, nullptr if no widget creator is associated with the editable + * parameter type. + */ +RA_DATAFLOW_API QWidget* createWidget( EditableParameterBase* editableParameter ); + +/** + * Update a widget from an editable parameter using the widget factory + * + * @param widget the widget to update + * @param editableParameter the data whose content will be transfered to the widget + * @return true if the update is done, false if thereis no updater associated with the + * editableParrameter type. + */ +RA_DATAFLOW_API bool updateWidget( QWidget* widget, EditableParameterBase* editableParameter ); + +/** + * Initialize the factory with pre-defined widgets according to the Radium NodeGraph predefined + * nodes. + */ +RA_DATAFLOW_API void initializeWidgetFactory(); + +} // namespace WidgetFactory + +} // namespace GraphEditor +} // namespace QtGui +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/QtGui/GraphEditor/images/copy.png b/src/Dataflow/QtGui/GraphEditor/images/copy.png new file mode 100644 index 00000000000..2aeb28288f5 Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/copy.png differ diff --git a/src/Dataflow/QtGui/GraphEditor/images/cut.png b/src/Dataflow/QtGui/GraphEditor/images/cut.png new file mode 100644 index 00000000000..54638e9386d Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/cut.png differ diff --git a/src/Dataflow/QtGui/GraphEditor/images/new.png b/src/Dataflow/QtGui/GraphEditor/images/new.png new file mode 100644 index 00000000000..12131b01008 Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/new.png differ diff --git a/src/Dataflow/QtGui/GraphEditor/images/open.png b/src/Dataflow/QtGui/GraphEditor/images/open.png new file mode 100644 index 00000000000..45fa2883a71 Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/open.png differ diff --git a/src/Dataflow/QtGui/GraphEditor/images/paste.png b/src/Dataflow/QtGui/GraphEditor/images/paste.png new file mode 100644 index 00000000000..c14425cad1f Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/paste.png differ diff --git a/src/Dataflow/QtGui/GraphEditor/images/save.png b/src/Dataflow/QtGui/GraphEditor/images/save.png new file mode 100644 index 00000000000..e65a29d5f17 Binary files /dev/null and b/src/Dataflow/QtGui/GraphEditor/images/save.png differ diff --git a/src/Dataflow/QtGui/filelist.cmake b/src/Dataflow/QtGui/filelist.cmake new file mode 100644 index 00000000000..ac4590ae925 --- /dev/null +++ b/src/Dataflow/QtGui/filelist.cmake @@ -0,0 +1,15 @@ +# ---------------------------------------------------- +# This file should not be generated by the radium script +# ---------------------------------------------------- + +set(dataflow_qtgui_sources GraphEditor/GraphEditorView.cpp GraphEditor/NodeAdapterModel.cpp + GraphEditor/GraphEditorWindow.cpp GraphEditor/WidgetFactory.cpp +) + +set(dataflow_qtgui_headers + GraphEditor/ConnectionStatusData.hpp GraphEditor/GraphEditorView.hpp + GraphEditor/GraphEditorWindow.hpp GraphEditor/NodeAdapterModel.hpp + GraphEditor/WidgetFactory.hpp +) + +set(dataflow_qtgui_resources GraphEditor/GraphEditor.qrc) diff --git a/src/Dataflow/QtGui/pch.hpp b/src/Dataflow/QtGui/pch.hpp new file mode 100644 index 00000000000..6f70f09beec --- /dev/null +++ b/src/Dataflow/QtGui/pch.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/src/Dataflow/RaDataflow.hpp b/src/Dataflow/RaDataflow.hpp new file mode 100644 index 00000000000..046321b7ce9 --- /dev/null +++ b/src/Dataflow/RaDataflow.hpp @@ -0,0 +1,21 @@ +#pragma once +#include + +/// Defines the correct macro to export dll symbols. +#if defined RA_DATAFLOW_EXPORTS +# define RA_DATAFLOW_API DLL_EXPORT +#elif defined RA_DATAFLOW_STATIC +# define RA_DATAFLOW_API +#else +# define RA_DATAFLOW_API DLL_IMPORT +#endif + +/// Allow to define initializers for modules that need to be initialized transparently +#define DATAFLOW_LIBRARY_INITIALIZER_DECL( f ) void f##__Initializer() + +#define DATAFLOW_LIBRARY_INITIALIZER_IMPL( f ) \ + struct f##__Initializer_t_ { \ + f##__Initializer_t_() { ::f##__Initializer(); } \ + }; \ + static f##__Initializer_t_ f##__Initializer__; \ + void f##__Initializer() diff --git a/src/Dataflow/Rendering/CMakeLists.txt b/src/Dataflow/Rendering/CMakeLists.txt new file mode 100644 index 00000000000..f678dc9431f --- /dev/null +++ b/src/Dataflow/Rendering/CMakeLists.txt @@ -0,0 +1,48 @@ +set(ra_dataflowrendering_target DataflowRendering) +list(APPEND CMAKE_MESSAGE_INDENT "[${ra_dataflowrendering_target}] ") + +project(${ra_dataflowrendering_target} LANGUAGES CXX VERSION ${Radium_VERSION}) + +include(filelist.cmake) + +# configure library +add_library( + ${ra_dataflowrendering_target} SHARED + ${dataflow_rendering_sources} ${dataflow_rendering_headers} ${dataflow_rendering_resources} +) + +# This one should be extracted directly from parent project properties. +target_compile_definitions(${ra_dataflowrendering_target} PRIVATE RA_DATAFLOW_EXPORTS) + +target_compile_options(${ra_dataflowrendering_target} PUBLIC ${RA_DEFAULT_COMPILE_OPTIONS}) + +add_dependencies(${ra_dataflowrendering_target} DataflowCore Engine) +target_link_libraries(${ra_dataflowrendering_target} PUBLIC DataflowCore Engine) + +message(STATUS "Configuring library ${ra_dataflowrendering_target} with standard settings") +configure_radium_target(${ra_dataflowrendering_target}) +# configure the library only. The package is a sub-package and should be configured independently +configure_radium_library( + TARGET ${ra_dataflowrendering_target} COMPONENT TARGET_DIR "Dataflow/Rendering" + FILES "${dataflow_rendering_headers}" +) +# Generate cmake package +configure_radium_package( + NAME ${ra_dataflowrendering_target} PACKAGE_CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in + PACKAGE_DIR "lib/cmake/Radium/${ra_dataflowrendering_target}" NAME_PREFIX "Radium" +) + +foreach(resourceDir ${dataflow_rendering_resources}) + install_target_resources( + TARGET ${ra_dataflowrendering_target} RESOURCES_INSTALL_DIR "Radium/Dataflow" + RESOURCES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${resourceDir} + ) +endforeach() + +set(RADIUM_COMPONENTS ${RADIUM_COMPONENTS} ${ra_dataflowrendering_target} PARENT_SCOPE) + +if(RADIUM_ENABLE_PCH) + target_precompile_headers(${ra_dataflowrendering_target} PRIVATE pch.hpp) +endif() + +list(REMOVE_AT CMAKE_MESSAGE_INDENT -1) diff --git a/src/Dataflow/Rendering/Config.cmake.in b/src/Dataflow/Rendering/Config.cmake.in new file mode 100644 index 00000000000..578e992e349 --- /dev/null +++ b/src/Dataflow/Rendering/Config.cmake.in @@ -0,0 +1,38 @@ +# -------------- Configuration of the Radium DataflowCore targets and definitions ----------------------- +# Setup Engine and check for dependencies + +if (DataflowRendering_FOUND AND NOT TARGET DataflowRendering) + set(Configure_DataflowRendering ON) + # verify dependencies + if(NOT DataflowCore_FOUND) + # if in source dir + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Core/RadiumDataflowCoreConfig.cmake") + set(DataflowCore_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Core/RadiumDataflowCoreConfig.cmake) + else() + # if in install dir + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake") + set(DataflowCore_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../DataflowCore/RadiumDataflowCoreConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::DataflowRendering: dependency DataflowCore not found") + set(Configure_DataflowRendering OFF) + endif() + endif() + endif() + if(NOT Engine_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../Engine/RadiumEngineConfig.cmake") + set(Engine_FOUND TRUE) + include(${CMAKE_CURRENT_LIST_DIR}/../Engine/RadiumEngineConfig.cmake) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::DataflowRendering: dependency Engine not found") + set(Configure_DataflowRendering OFF) + endif() + endif() +endif() + +if(Configure_DataflowRendering) + include("${CMAKE_CURRENT_LIST_DIR}/../Dataflow/Rendering/DataflowRenderingTargets.cmake") +endif() diff --git a/src/Dataflow/Rendering/Nodes/AntiAliasing/FxaaNode.cpp b/src/Dataflow/Rendering/Nodes/AntiAliasing/FxaaNode.cpp new file mode 100644 index 00000000000..f904bd05398 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/AntiAliasing/FxaaNode.cpp @@ -0,0 +1,174 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Engine::Data; + +FxaaNode::FxaaNode( const std::string& name ) : RenderingNode( name, getTypename() ) { + addInput( m_inColor ); + m_inColor->mustBeLinked(); + + Ra::Engine::Data::TextureParameters colorTexParams = { "FXAA image", + gl::GL_TEXTURE_2D, + 1, + 1, + 1, + gl::GL_RGBA, + gl::GL_RGBA32F, + gl::GL_FLOAT, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_LINEAR, + gl::GL_LINEAR, + nullptr }; + m_colorTexture = new Ra::Engine::Data::Texture( colorTexParams ); + + addOutput( m_outColor, m_colorTexture ); +} + +void FxaaNode::init() { + m_framebuffer = new globjects::Framebuffer(); +} + +void FxaaNode::destroy() { + delete m_framebuffer; + delete m_colorTexture; +} + +void FxaaNode::resize( uint32_t width, uint32_t height ) { + m_colorTexture->resize( width, height ); +} + +bool FxaaNode::execute() { + m_framebuffer->bind(); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_colorTexture->texture() ); + const gl::GLenum buffers[] = { gl::GL_COLOR_ATTACHMENT0 }; + gl::glDrawBuffers( 1, buffers ); + gl::glDisable( gl::GL_DEPTH_TEST ); + gl::glDepthMask( gl::GL_FALSE ); + gl::glColorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + gl::glDisable( gl::GL_BLEND ); + + float clearBlack[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + gl::glClearBufferfv( gl::GL_COLOR, 0, clearBlack ); + + m_shader->bind(); + m_shader->setUniform( "tex1_sampler", &m_inColor->getData(), 0 ); + m_quadMesh->render( m_shader ); + m_framebuffer->unbind(); + return true; +} + +bool FxaaNode::initInternalShaders() { + if ( m_quadMesh == nullptr ) { + Ra::Core::Geometry::TriangleMesh mesh = + Ra::Core::Geometry::makeZNormalQuad( Ra::Core::Vector2( -1.f, 1.f ) ); + auto qm = std::make_unique( "caller" ); + qm->loadGeometry( std::move( mesh ) ); + m_quadMesh = std::move( qm ); + m_quadMesh->updateGL(); + } + + if ( m_shader == nullptr ) { + const std::string composeVertexShader { "layout (location = 0) in vec3 in_position;\n" + "out vec2 varTexcoord;\n" + "void main()\n" + "{\n" + " gl_Position = vec4(in_position, 1.0);\n" + " varTexcoord = (in_position.xy + 1.0) * 0.5;\n" + "}\n" }; + const std::string composeFragmentShader { + "layout (location = 0) out vec4 out_tex;\n" + "uniform sampler2D tex1_sampler;\n" + "in vec2 varTexcoord;\n" + "const float THRESHOLD = 0.0312;\n" + "const float RELATIVE_THRESHOLD = 0.125;\n" + "void main() {\n" + "vec2 texSize = vec2(textureSize(tex1_sampler, 0));\n" + "vec2 texelSize = 1.0 / texSize;\n" + "vec3 n = texture(tex1_sampler, varTexcoord + (vec2(0.0, -1.0) * texelSize)).rgb;\n" + "vec3 s = texture(tex1_sampler, varTexcoord + (vec2(0.0, 1.0) * texelSize)).rgb;\n" + "vec3 e = texture(tex1_sampler, varTexcoord + (vec2(1.0, 0.0) * texelSize)).rgb;\n" + "vec3 w = texture(tex1_sampler, varTexcoord + (vec2(-1.0, 0.0) * texelSize)).rgb;\n" + "vec3 m = texture(tex1_sampler, varTexcoord).rgb;\n" + "vec3 brightnessCoefficients = vec3(0.2126, 0.7152, 0.0722);\n" + "float brightnessN = dot(n, brightnessCoefficients);\n" + "float brightnessS = dot(s, brightnessCoefficients);\n" + "float brightnessE = dot(e, brightnessCoefficients);\n" + "float brightnessW = dot(w, brightnessCoefficients);\n" + "float brightnessM = dot(m, brightnessCoefficients);\n" + "float brightnessMin = min(brightnessM, min(min(brightnessN, brightnessS), " + "min(brightnessE, brightnessW)));\n" + "float brightnessMax = max(brightnessM, max(max(brightnessN, brightnessS), " + "max(brightnessE, brightnessW)));\n" + "float contrast = brightnessMax - brightnessMin;\n" + "float threshold = max(THRESHOLD, RELATIVE_THRESHOLD * brightnessMax);\n" + "if (contrast < threshold) {\n" + "out_tex = vec4(m, 1.0);\n" + "}\n" + "else {\n" + "vec3 nw = texture(tex1_sampler, varTexcoord + (vec2(-1.0, -1.0) * texelSize)).rgb;\n" + "vec3 ne = texture(tex1_sampler, varTexcoord + (vec2(1.0, -1.0) * texelSize)).rgb;\n" + "vec3 sw = texture(tex1_sampler, varTexcoord + (vec2(-1.0, 1.0) * texelSize)).rgb;\n" + "vec3 se = texture(tex1_sampler, varTexcoord + (vec2(1.0, 1.0) * texelSize)).rgb;\n" + "float brightnessNW = dot(nw, brightnessCoefficients);\n" + "float brightnessNE = dot(ne, brightnessCoefficients);\n" + "float brightnessSW = dot(sw, brightnessCoefficients);\n" + "float brightnessSE = dot(se, brightnessCoefficients);\n" + "float factor = 2 * (brightnessN + brightnessS + brightnessE + brightnessW);\n" + "factor += (brightnessNW + brightnessNE + brightnessSW + brightnessSE);\n" + "factor *= (1.0 / 12.0);\n" + "factor = abs(factor - brightnessM);\n" + "factor = clamp(factor / contrast, 0.0, 1.0);\n" + "factor = smoothstep(0.0, 1.0, factor);\n" + "factor = factor * factor;\n" + "float horizontal = abs(brightnessN + brightnessS - (2 * brightnessM)) * 2 +\n" + "abs(brightnessNE + brightnessSE - (2 * brightnessE)) +\n" + "abs(brightnessNW + brightnessSW - (2 * brightnessW));\n" + "float vertical = abs(brightnessE + brightnessW - (2 * brightnessM)) * 2 +\n" + "abs(brightnessNE + brightnessSE - (2 * brightnessN)) +\n" + "abs(brightnessNW + brightnessSW - (2 * brightnessS));\n" + "bool isHorizontal = horizontal > vertical;\n" + "float pixelStep = isHorizontal ? texelSize.y : texelSize.x;\n" + "float posBrightness = isHorizontal ? brightnessS : brightnessE;\n" + "float negBrightness = isHorizontal ? brightnessN : brightnessW;\n" + "float posGradient = abs(posBrightness - brightnessM);\n" + "float negGradient = abs(negBrightness - brightnessM);\n" + "pixelStep *= (posGradient < negGradient) ? -1 : 1;\n" + "vec2 blendUV = varTexcoord;\n" + "if (isHorizontal) {\n" + "blendUV.y = varTexcoord.y + (pixelStep * factor);\n" + "}\n" + "else {\n" + "blendUV.x = varTexcoord.x + (pixelStep * factor); \n" + "}\n" + "out_tex = texture(tex1_sampler, blendUV);\n" + "}\n" + "}" }; + + Ra::Engine::Data::ShaderConfiguration config { "ComposeMax" }; + config.addShaderSource( Ra::Engine::Data::ShaderType::ShaderType_VERTEX, + composeVertexShader ); + config.addShaderSource( Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT, + composeFragmentShader ); + auto added = m_shaderMngr->addShaderProgram( config ); + if ( added ) { m_shader = added.value(); } + } + return ( m_shader != nullptr && m_quadMesh != nullptr ); +} + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/AntiAliasing/FxaaNode.hpp b/src/Dataflow/Rendering/Nodes/AntiAliasing/FxaaNode.hpp new file mode 100644 index 00000000000..f6ecb13778f --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/AntiAliasing/FxaaNode.hpp @@ -0,0 +1,46 @@ +#pragma once +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { +using namespace Ra::Dataflow::Core; +using namespace Ra::Engine::Data; + +/** + * \brief compute antialiasing in image space from color buffer. + * + */ +class RA_DATAFLOW_API FxaaNode : public RenderingNode +{ + public: + explicit FxaaNode( const std::string& name ); + + void init() override; + bool execute() override; + void destroy() override; + void resize( uint32_t width, uint32_t height ) override; + bool initInternalShaders() override; + + static const std::string getTypename() { return "FXAA Node"; } + + private: + Ra::Engine::Data::Texture* m_colorTexture { nullptr }; + + globjects::Framebuffer* m_framebuffer { nullptr }; + std::unique_ptr m_quadMesh { nullptr }; + const Ra::Engine::Data::ShaderProgram* m_shader { nullptr }; + + PortIn* m_inColor { new PortIn( "color", this ) }; + PortOut* m_outColor { new PortOut( "Beauty", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/ClearColorNode.cpp b/src/Dataflow/Rendering/Nodes/RenderNodes/ClearColorNode.cpp new file mode 100644 index 00000000000..69034bc9147 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/ClearColorNode.cpp @@ -0,0 +1,121 @@ +#include + +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Engine::Data; + +using EnvironmentType = std::shared_ptr; + +ClearColorNode::ClearColorNode( const std::string& name ) : RenderingNode( name, getTypename() ) { + addInput( m_portInColorTex ); + // m_portInColorTex->mustBeLinked(); + addInput( m_portInClearColor ); + addInput( m_portInEnvmap ); + addInput( m_portInCamera ); + + addOutput( m_portOutColorTex, m_colorTexture ); + + auto editableColor = new EditableParameter( "clear color", m_editableClearColor ); + addEditableParameter( editableColor ); +} + +void ClearColorNode::init() { + Ra::Engine::Data::TextureParameters texParams { getInstanceName() + " (Color)", + gl::GL_TEXTURE_2D, + 1, + 1, + 1, + gl::GL_RGBA, + gl::GL_RGBA32F, + gl::GL_FLOAT, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_LINEAR, + gl::GL_LINEAR, + nullptr }; + m_texture = new Ra::Engine::Data::Texture( texParams ); + m_framebuffer = new globjects::Framebuffer(); +} + +void ClearColorNode::destroy() { + delete m_texture; + delete m_framebuffer; +} + +void ClearColorNode::resize( uint32_t w, uint32_t h ) { + if ( !m_portInColorTex->isLinked() ) { m_texture->resize( w, h ); } +} + +bool ClearColorNode::execute() { + + // Color texture + if ( !m_portInColorTex->isLinked() ) { m_colorTexture = m_texture; } + else { m_colorTexture = &m_portInColorTex->getData(); } + m_portOutColorTex->setData( m_colorTexture ); + + // Clear color + Scalar* clearColor = m_editableClearColor.data(); + if ( m_portInClearColor->isLinked() ) { clearColor = m_portInClearColor->getData().data(); } + + // Envmap + EnvironmentTexture* envmap { nullptr }; + if ( m_portInEnvmap->isLinked() && m_portInCamera->isLinked() ) { + envmap = m_portInEnvmap->getData().get(); + } + + m_framebuffer->bind(); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_colorTexture->texture() ); + const gl::GLenum buffers[] = { gl::GL_COLOR_ATTACHMENT0 }; + gl::glDrawBuffers( 1, buffers ); + gl::glDisable( gl::GL_BLEND ); + + if ( envmap ) { + gl::glDepthMask( gl::GL_FALSE ); + gl::glColorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + gl::glDisable( gl::GL_DEPTH_TEST ); + envmap->render( m_portInCamera->getData(), false ); + gl::glDepthMask( gl::GL_TRUE ); + gl::glEnable( gl::GL_DEPTH_TEST ); + } + else { + clearColor[3] = 0_ra; + gl::glClearBufferfv( gl::GL_COLOR, 0, clearColor ); + } + + m_framebuffer->detach( gl::GL_COLOR_ATTACHMENT0 ); + m_framebuffer->unbind(); + return true; +} + +void ClearColorNode::toJsonInternal( nlohmann::json& data ) const { + auto c = ColorType::linearRGBTosRGB( m_editableClearColor ); + std::array color { { c.x(), c.y(), c.z() } }; + data["clearColor"] = color; +} + +bool ClearColorNode::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "clearColor" ) ) { + std::array c = data["clearColor"]; + m_editableClearColor = ColorType::sRGBToLinearRGB( ColorType( c[0], c[1], c[2] ) ); + } + else { + m_editableClearColor = + ColorType::sRGBToLinearRGB( ColorType( 42, 42, 42 ) * 1_ra / 255_ra ); + } + return true; +} + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/ClearColorNode.hpp b/src/Dataflow/Rendering/Nodes/RenderNodes/ClearColorNode.hpp new file mode 100644 index 00000000000..345bd2b23c7 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/ClearColorNode.hpp @@ -0,0 +1,65 @@ +#pragma once +#include + +#include + +#include + +#include + +namespace Ra { + +namespace Engine { +namespace Data { +class EnvironmentTexture; +} +} // namespace Engine +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Dataflow::Core; +using namespace Ra::Engine::Data; +using EnvironmentType = std::shared_ptr; + +/** + * \brief Initialize the given image either using a constant color or by rendering a sky box. + * + */ +class RA_DATAFLOW_API ClearColorNode : public RenderingNode +{ + public: + explicit ClearColorNode( const std::string& name ); + + void init() override; + bool execute() override; + void destroy() override; + + void resize( uint32_t, uint32_t ) override; + + static const std::string getTypename() { return "Clear Color Pass"; } + + protected: + void toJsonInternal( nlohmann::json& data ) const override; + bool fromJsonInternal( const nlohmann::json& data ) override; + + private: + // This texture will be used if the port m_portInColorTex is not linked + Ra::Engine::Data::Texture* m_texture { nullptr }; + // This texture will be used if the port m_portInColorTex is linked + Ra::Engine::Data::Texture* m_colorTexture { nullptr }; + globjects::Framebuffer* m_framebuffer { nullptr }; + + ColorType m_editableClearColor { ColorType::Grey( 0.12_ra ) }; + + PortIn* m_portInColorTex { new PortIn( "texture", this ) }; + PortIn* m_portInClearColor { new PortIn( "clear color", this ) }; + PortIn* m_portInEnvmap { new PortIn( "environment", this ) }; + PortIn* m_portInCamera { new PortIn( "camera", this ) }; + PortOut* m_portOutColorTex { new PortOut( "Beauty", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/EmissivityRenderNode.cpp b/src/Dataflow/Rendering/Nodes/RenderNodes/EmissivityRenderNode.cpp new file mode 100644 index 00000000000..0a5252f5870 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/EmissivityRenderNode.cpp @@ -0,0 +1,141 @@ +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Engine::Data; + +EmissivityNode::EmissivityNode( const std::string& name ) : RenderingNode( name, getTypename() ) { + addInput( m_inObjects ); + m_inObjects->mustBeLinked(); + addInput( m_inCamera ); + m_inCamera->mustBeLinked(); + + // TODO, allow to not provide the two following inputs, then using internal textures ? + addInput( m_inColor ); + m_inColor->mustBeLinked(); + addInput( m_inDepth ); + m_inDepth->mustBeLinked(); + + addInput( m_inAo ); + + addOutput( m_outColor, m_colorTexture ); +} + +void EmissivityNode::init() { + m_framebuffer = new globjects::Framebuffer(); + + float blankAO[4] = { 1.f, 1.f, 1.f, 1.f }; + Ra::Engine::Data::TextureParameters texParams; + texParams.target = gl::GL_TEXTURE_2D; + texParams.width = 1; + texParams.height = 1; + texParams.internalFormat = gl::GL_RGBA32F; + texParams.format = gl::GL_RGBA; + texParams.type = gl::GL_FLOAT; + texParams.minFilter = gl::GL_NEAREST; + texParams.magFilter = gl::GL_NEAREST; + texParams.name = "Blank AO"; + texParams.texels = &blankAO; + m_blankAO = new Ra::Engine::Data::Texture( texParams ); + m_blankAO->initializeGL(); + + m_nodeState = new globjects::State( globjects::State::DeferredMode ); + m_nodeState->enable( gl::GL_DEPTH_TEST ); + m_nodeState->depthFunc( gl::GL_LEQUAL ); + m_nodeState->depthMask( gl::GL_FALSE ); + m_nodeState->colorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + m_nodeState->blendFuncSeparate( gl::GL_ONE, gl::GL_DST_ALPHA, gl::GL_ONE, gl::GL_ZERO ); + m_nodeState->enable( gl::GL_BLEND ); +} + +void EmissivityNode::destroy() { + delete m_framebuffer; + delete m_blankAO; + + delete m_nodeState; +} + +void EmissivityNode::resize( uint32_t, uint32_t ) {} + +void EmissivityNode::buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const { + std::string resourcesRootDir = m_resourceDir + "Shaders/EmissivityNode/"; + auto mat = const_cast( ro )->getMaterial(); + // Volumes are not used in EmissivityPass + if ( mat->getMaterialAspect() == Ra::Engine::Data::Material::MaterialAspect::MAT_DENSITY ) { + return; + } + if ( auto cfg = Ra::Engine::Data::ShaderConfigurationFactory::getConfiguration( + { "EmissivityNode::" + mat->getMaterialName() } ) ) { + rt.setConfiguration( *cfg, m_idx ); + } + else { + // Build the shader configuration + Ra::Engine::Data::ShaderConfiguration theConfig { + { "EmissivityNode::" + mat->getMaterialName() }, + m_resourceDir + "Shaders/shader_nolight.vert.glsl", + resourcesRootDir + "shader.frag.glsl" }; + // add the material interface to the fragment shader + theConfig.addInclude( "\"" + mat->getMaterialName() + ".glsl\"", + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + // Add to the ShaderConfigManager + Ra::Engine::Data::ShaderConfigurationFactory::addConfiguration( theConfig ); + // Add to the RenderTechniq + rt.setConfiguration( theConfig, m_idx ); + } + rt.setParametersProvider( mat, m_idx ); +} + +bool EmissivityNode::execute() { + // Get parameters + // Render objects + auto& renderObjects = m_inObjects->getData(); + // Cameras + auto& camera = m_inCamera->getData(); + + // Color tex + m_colorTexture = &m_inColor->getData(); + m_outColor->setData( m_colorTexture ); + + // Depth buffer + auto& depthBuffer = m_inDepth->getData(); + + // SSAO texture + auto aoTexture = m_inAo->isLinked() ? &m_inAo->getData() : m_blankAO; + Ra::Engine::Data::RenderParameters inPassParams; + inPassParams.addParameter( "amb_occ_sampler", aoTexture ); + + // Compute the result + m_framebuffer->bind(); + m_framebuffer->attachTexture( gl::GL_DEPTH_ATTACHMENT, depthBuffer.texture() ); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_colorTexture->texture() ); + + const gl::GLenum buffers[] = { gl::GL_COLOR_ATTACHMENT0 }; + gl::glDrawBuffers( 1, buffers ); + + auto currentState = globjects::State::currentState(); + m_nodeState->apply(); + + for ( const auto& ro : renderObjects ) { + ro->render( inPassParams, camera, m_idx ); + } + + currentState->apply(); + + m_framebuffer->unbind(); + + // Set output port + // here, they are already set by constructor + return true; +} + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/EmissivityRenderNode.hpp b/src/Dataflow/Rendering/Nodes/RenderNodes/EmissivityRenderNode.hpp new file mode 100644 index 00000000000..6a5027df477 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/EmissivityRenderNode.hpp @@ -0,0 +1,67 @@ +#pragma once +#include + +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { +using namespace Ra::Dataflow::Core; +/** + * \brief Render a scene so that only the emissivity is computed. + * This node expect to have as input the depth buffer of the scene (from any depth-prepass) and a + * color buffer with the background color set. + * This node will replace the color ofe each pixel in the color buffer by the computed emissivity + * for all visible fragments. + * pixels not covered by a fragments are left unchanged. + * + */ +class RA_DATAFLOW_API EmissivityNode : public RenderingNode +{ + public: + explicit EmissivityNode( const std::string& name ); + + void init() override; + + bool execute() override; + + void destroy() override; + + void resize( uint32_t width, uint32_t height ) override; + + void buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const override; + + bool hasRenderTechnique() override { return true; } + + static const std::string getTypename() { return "Emissivity Render Node"; } + + private: + // This texture is not owned by the node, it is just an alias of input/output color texture + Ra::Engine::Data::Texture* m_colorTexture { nullptr }; + + Ra::Engine::Data::Texture* m_blankAO { nullptr }; + + globjects::Framebuffer* m_framebuffer { nullptr }; + + globjects::State* m_nodeState; + + PortIn>* m_inObjects { + new PortIn>( "objects", this ) }; + PortIn* m_inCamera { new PortIn( "camera", this ) }; + + PortIn* m_inDepth { new PortIn( "depth", this ) }; + PortIn* m_inColor { new PortIn( "color", this ) }; + PortIn* m_inAo { new PortIn( "AO", this ) }; + + PortOut* m_outColor { new PortOut( "Beauty", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/EnvironmentLightingNode.cpp b/src/Dataflow/Rendering/Nodes/RenderNodes/EnvironmentLightingNode.cpp new file mode 100644 index 00000000000..01d15c9b862 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/EnvironmentLightingNode.cpp @@ -0,0 +1,163 @@ +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Engine::Data; + +EnvironmentLightingNode::EnvironmentLightingNode( const std::string& name ) : + RenderingNode( name, getTypename() ) { + + addInput( m_inObjects ); + m_inObjects->mustBeLinked(); + addInput( m_inCamera ); + m_inCamera->mustBeLinked(); + + addInput( m_inEnvironment ); + // if no envmap available, the node will be pass_through + + // TODO, allow to not provide the two following inputs, then using internal textures ? + addInput( m_inColor ); + m_inColor->mustBeLinked(); + addInput( m_inDepth ); + m_inDepth->mustBeLinked(); + + addInput( m_inAo ); + addOutput( m_outColor, m_colorTexture ); +} + +void EnvironmentLightingNode::init() { + m_framebuffer = new globjects::Framebuffer(); + + float blankAO[4] = { 1.f, 1.f, 1.f, 1.f }; + Ra::Engine::Data::TextureParameters texParams; + texParams.target = gl::GL_TEXTURE_2D; + texParams.width = 1; + texParams.height = 1; + texParams.internalFormat = gl::GL_RGBA32F; + texParams.format = gl::GL_RGBA; + texParams.type = gl::GL_FLOAT; + texParams.minFilter = gl::GL_NEAREST; + texParams.magFilter = gl::GL_NEAREST; + texParams.name = "Blank AO"; + texParams.texels = &blankAO; + m_blankAO = new Ra::Engine::Data::Texture( texParams ); + m_blankAO->initializeGL(); + + m_nodeState = new globjects::State( globjects::State::DeferredMode ); + m_nodeState->enable( gl::GL_DEPTH_TEST ); + m_nodeState->depthFunc( gl::GL_LEQUAL ); + m_nodeState->depthMask( gl::GL_FALSE ); + m_nodeState->colorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + // m_nodeState->blendFunc( gl::GL_ONE, gl::GL_DST_ALPHA ); + m_nodeState->blendFuncSeparate( gl::GL_ONE, gl::GL_DST_ALPHA, gl::GL_ONE, gl::GL_ZERO ); + m_nodeState->enable( gl::GL_BLEND ); +} + +void EnvironmentLightingNode::destroy() { + delete m_framebuffer; + delete m_blankAO; + + delete m_nodeState; +} + +void EnvironmentLightingNode::resize( uint32_t, uint32_t ) {} + +void EnvironmentLightingNode::buildRenderTechnique( + const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const { + + auto mat = const_cast( ro )->getMaterial(); + // Volumes are not (yet) managed by EnvironmentLightingNode + if ( mat->getMaterialAspect() == Ra::Engine::Data::Material::MaterialAspect::MAT_DENSITY ) { + return; + } + if ( auto cfg = Ra::Engine::Data::ShaderConfigurationFactory::getConfiguration( + { "EnvironmentLightingNode::" + mat->getMaterialName() } ) ) { + rt.setConfiguration( *cfg, m_idx ); + } + else { + std::string nodeShaderDir = m_resourceDir + "Shaders/EnvLightNode/"; + // Build the shader configuration + Ra::Engine::Data::ShaderConfiguration theConfig { + { "EnvironmentLightingNode::" + mat->getMaterialName() }, + m_resourceDir + "Shaders/shader_nolight.vert.glsl", + nodeShaderDir + "shader.frag.glsl" }; + // add the material interface to the fragment shader + theConfig.addInclude( "\"" + mat->getMaterialName() + ".glsl\"", + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + // Add to the ShaderConfigManager + Ra::Engine::Data::ShaderConfigurationFactory::addConfiguration( theConfig ); + // Add to the RenderTechnique + rt.setConfiguration( theConfig, m_idx ); + } + rt.setParametersProvider( mat, m_idx ); +} + +bool EnvironmentLightingNode::execute() { + // Get parameters + // Render objects + auto& renderObjects = m_inObjects->getData(); + // Cameras + auto& camera = m_inCamera->getData(); + + // Color tex + m_colorTexture = &m_inColor->getData(); + m_outColor->setData( m_colorTexture ); + + // Depth buffer + auto& depthBuffer = m_inDepth->getData(); + + // SSAO texture + auto aoTexture = m_inAo->isLinked() ? &m_inAo->getData() : m_blankAO; + Ra::Engine::Data::RenderParameters inPassParams; + inPassParams.addParameter( "amb_occ_sampler", aoTexture ); + + // Environment map + auto envmap = m_inEnvironment->isLinked() ? m_inEnvironment->getData().get() : nullptr; + if ( !envmap ) { return true; } + + // TODO, verify why this is needed .... + envmap->updateGL(); + + inPassParams.addParameter( "redShCoeffs", envmap->getShMatrix( 0 ) ); + inPassParams.addParameter( "greenShCoeffs", envmap->getShMatrix( 1 ) ); + inPassParams.addParameter( "blueShCoeffs", envmap->getShMatrix( 2 ) ); + inPassParams.addParameter( "envTexture", envmap->getEnvironmentTexture() ); + inPassParams.addParameter( "envStrength", envmap->getStrength() ); + int numLod = std::log2( std::min( envmap->getEnvironmentTexture()->width(), + envmap->getEnvironmentTexture()->height() ) ); + inPassParams.addParameter( "numLod", numLod ); + + // Compute the result + m_framebuffer->bind(); + m_framebuffer->attachTexture( gl::GL_DEPTH_ATTACHMENT, depthBuffer.texture() ); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_colorTexture->texture() ); + + const gl::GLenum buffers[] = { gl::GL_COLOR_ATTACHMENT0 }; + gl::glDrawBuffers( 1, buffers ); + + auto currentState = globjects::State::currentState(); + m_nodeState->apply(); + for ( const auto& ro : renderObjects ) { + ro->render( inPassParams, camera, m_idx ); + } + + currentState->apply(); + + m_framebuffer->unbind(); + + // Set output port + // here, they are already set by constructor + return true; +} + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/EnvironmentLightingNode.hpp b/src/Dataflow/Rendering/Nodes/RenderNodes/EnvironmentLightingNode.hpp new file mode 100644 index 00000000000..39e68859a61 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/EnvironmentLightingNode.hpp @@ -0,0 +1,67 @@ +#pragma once +#include + +#include + +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +using namespace Ra::Engine::Data; + +// This should be made unique instead of duplicated in EnvMapSource +using EnvmapPtrType = std::shared_ptr; + +namespace Nodes { +using namespace Ra::Dataflow::Core; +/** + * \brief Render a scene using an environment map as light source. + * + */ +class RA_DATAFLOW_API EnvironmentLightingNode : public RenderingNode +{ + public: + explicit EnvironmentLightingNode( const std::string& name ); + + void init() override; + + bool execute() override; + + void destroy() override; + + void resize( uint32_t width, uint32_t height ) override; + + void buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const override; + + bool hasRenderTechnique() override { return true; } + + static const std::string getTypename() { return "Env Lighting Node"; } + + private: + Ra::Engine::Data::Texture* m_colorTexture; + Ra::Engine::Data::Texture* m_blankAO { nullptr }; + + globjects::Framebuffer* m_framebuffer { nullptr }; + + globjects::State* m_nodeState; + + PortIn* m_inColor { new PortIn( "color", this ) }; + PortIn* m_inDepth { new PortIn( "depth", this ) }; + PortIn* m_inAo { new PortIn( "AO", this ) }; + PortIn* m_inEnvironment { new PortIn( "envmap", this ) }; + PortIn>* m_inObjects { + new PortIn>( "objects", this ) }; + PortIn* m_inCamera { new PortIn( "camera", this ) }; + + PortOut* m_outColor { new PortOut( "Beauty", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/GeometryAovsNode.cpp b/src/Dataflow/Rendering/Nodes/RenderNodes/GeometryAovsNode.cpp new file mode 100644 index 00000000000..edce0331fe3 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/GeometryAovsNode.cpp @@ -0,0 +1,149 @@ +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Engine::Data; + +GeometryAovsNode::GeometryAovsNode( const std::string& name ) : + RenderingNode( name, getTypename() ) { + addInput( m_inObjects ); + m_inObjects->mustBeLinked(); + addInput( m_inCamera ); + m_inCamera->mustBeLinked(); + + // Create internal texture + Ra::Engine::Data::TextureParameters texParams; + texParams.target = gl::GL_TEXTURE_2D; + + texParams.internalFormat = gl::GL_RGBA32F; + texParams.format = gl::GL_RGBA; + texParams.type = gl::GL_FLOAT; + texParams.minFilter = gl::GL_NEAREST; + texParams.magFilter = gl::GL_NEAREST; + texParams.name = "world pos"; + m_posInWorldTexture = new Ra::Engine::Data::Texture( texParams ); + + texParams.minFilter = gl::GL_LINEAR; + texParams.magFilter = gl::GL_LINEAR; + texParams.name = "world normal"; + m_normalInWorldTexture = new Ra::Engine::Data::Texture( texParams ); + + texParams.internalFormat = gl::GL_DEPTH_COMPONENT24; + texParams.format = gl::GL_DEPTH_COMPONENT; + texParams.type = gl::GL_UNSIGNED_INT; + texParams.minFilter = gl::GL_NEAREST; + texParams.magFilter = gl::GL_NEAREST; + texParams.name = "depth"; + m_depthTexture = new Ra::Engine::Data::Texture( texParams ); + + addOutput( m_outDepthTex, m_depthTexture ); + addOutput( m_outNormalInWorldTex, m_normalInWorldTexture ); + addOutput( m_outPosInWorldTex, m_posInWorldTexture ); +} + +void GeometryAovsNode::init() { + m_framebuffer = new globjects::Framebuffer(); + + m_nodeState = new globjects::State( globjects::State::DeferredMode ); + m_nodeState->enable( gl::GL_DEPTH_TEST ); + m_nodeState->depthMask( gl::GL_TRUE ); + m_nodeState->depthFunc( gl::GL_LESS ); + m_nodeState->colorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + m_nodeState->disable( gl::GL_BLEND ); +} + +void GeometryAovsNode::destroy() { + delete m_depthTexture; + delete m_normalInWorldTexture; + delete m_posInWorldTexture; + + delete m_nodeState; + delete m_framebuffer; +} + +void GeometryAovsNode::resize( uint32_t width, uint32_t height ) { + m_depthTexture->resize( width, height ); + m_posInWorldTexture->resize( width, height ); + m_normalInWorldTexture->resize( width, height ); + + m_framebuffer->bind(); + m_framebuffer->attachTexture( gl::GL_DEPTH_ATTACHMENT, m_depthTexture->texture() ); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_posInWorldTexture->texture() ); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT1, m_normalInWorldTexture->texture() ); + m_framebuffer->unbind(); +} + +void GeometryAovsNode::buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const { + std::string resourcesRootDir = m_resourceDir + "Shaders/GeometryAovs/"; + auto mat = const_cast( ro )->getMaterial(); + // Volumes are not used in geomPrepass + if ( mat->getMaterialAspect() == Ra::Engine::Data::Material::MaterialAspect::MAT_DENSITY ) { + return; + } + if ( auto cfg = Ra::Engine::Data::ShaderConfigurationFactory::getConfiguration( + { "GeometryAovsNode::" + mat->getMaterialName() } ) ) { + rt.setConfiguration( *cfg, m_idx ); + } + else { + // Build the shader configuration + Ra::Engine::Data::ShaderConfiguration theConfig { + { "GeometryAovsNode::" + mat->getMaterialName() }, + m_resourceDir + "Shaders/shader_nolight.vert.glsl", + resourcesRootDir + "shader.frag.glsl" }; + // add the material interface to the fragment shader + theConfig.addInclude( "\"" + mat->getMaterialName() + ".glsl\"", + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + // Add to the ShaderConfigManager + Ra::Engine::Data::ShaderConfigurationFactory::addConfiguration( theConfig ); + // Add to the RenderTechniq + rt.setConfiguration( theConfig, m_idx ); + } + rt.setParametersProvider( mat, m_idx ); +} + +bool GeometryAovsNode::execute() { + // Get parameters + // Render objects + auto& renderObjects = m_inObjects->getData(); + // Cameras + auto& camera = m_inCamera->getData(); + + // Compute the result + m_framebuffer->bind(); + + const gl::GLenum buffers[] = { gl::GL_COLOR_ATTACHMENT0, gl::GL_COLOR_ATTACHMENT1 }; + gl::glDrawBuffers( 2, buffers ); + + auto currentState = globjects::State::currentState(); + m_nodeState->apply(); + + float clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + float clearDepth = 1.0f; + gl::glClearBufferfv( gl::GL_COLOR, 0, clearColor ); + gl::glClearBufferfv( gl::GL_COLOR, 1, clearColor ); + gl::glClearBufferfv( gl::GL_DEPTH, 0, &clearDepth ); + + Ra::Engine::Data::RenderParameters inPassParams; + for ( const auto& ro : renderObjects ) { + ro->render( inPassParams, camera, m_idx ); + } + + currentState->apply(); + m_framebuffer->unbind(); + + // Set output port + // here, they are already set by constructor + return true; +} + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/GeometryAovsNode.hpp b/src/Dataflow/Rendering/Nodes/RenderNodes/GeometryAovsNode.hpp new file mode 100644 index 00000000000..12d4a7d83ed --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/GeometryAovsNode.hpp @@ -0,0 +1,63 @@ +#pragma once +#include + +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { +using namespace Ra::Dataflow::Core; +/** + * \brief Render a scene into several geometry buffers. + * - Image space fragment detph + * - World space fragment normal + * - World space fragment position + * + */ +class RA_DATAFLOW_API GeometryAovsNode : public RenderingNode +{ + public: + explicit GeometryAovsNode( const std::string& name ); + + void init() override; + + bool execute() override; + + void destroy() override; + + void resize( uint32_t width, uint32_t height ) override; + + void buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const override; + + bool hasRenderTechnique() override { return true; } + + static const std::string getTypename() { return "Geometry AOVs"; } + + private: + Ra::Engine::Data::Texture* m_depthTexture { nullptr }; + Ra::Engine::Data::Texture* m_posInWorldTexture { nullptr }; + Ra::Engine::Data::Texture* m_normalInWorldTexture { nullptr }; + + globjects::Framebuffer* m_framebuffer { nullptr }; + + globjects::State* m_nodeState; + + PortIn>* m_inObjects { + new PortIn>( "objects", this ) }; + PortIn* m_inCamera { new PortIn( "camera", this ) }; + + PortOut* m_outDepthTex { new PortOut( "depth", this ) }; + PortOut* m_outPosInWorldTex { new PortOut( "world pos", this ) }; + PortOut* m_outNormalInWorldTex { + new PortOut( "world normal", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/LocalLightingNode.cpp b/src/Dataflow/Rendering/Nodes/RenderNodes/LocalLightingNode.cpp new file mode 100644 index 00000000000..93afa01af27 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/LocalLightingNode.cpp @@ -0,0 +1,151 @@ +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Engine::Data; + +LocalLightingNode::LocalLightingNode( const std::string& name ) : + RenderingNode( name, getTypename() ) { + + addInput( m_inObjects ); + m_inObjects->mustBeLinked(); + addInput( m_inLights ); + m_inLights->mustBeLinked(); + addInput( m_inCamera ); + m_inCamera->mustBeLinked(); + + // TODO, allow to not provide the two following inputs, then using internal textures ? + addInput( m_inColor ); + m_inColor->mustBeLinked(); + addInput( m_inDepth ); + m_inDepth->mustBeLinked(); + + addInput( m_inAo ); + addOutput( m_outColor, m_colorTexture ); +} + +void LocalLightingNode::init() { + m_framebuffer = new globjects::Framebuffer(); + + float blankAO[4] = { 1.f, 1.f, 1.f, 1.f }; + Ra::Engine::Data::TextureParameters texParams; + texParams.target = gl::GL_TEXTURE_2D; + texParams.width = 1; + texParams.height = 1; + texParams.internalFormat = gl::GL_RGBA32F; + texParams.format = gl::GL_RGBA; + texParams.type = gl::GL_FLOAT; + texParams.minFilter = gl::GL_NEAREST; + texParams.magFilter = gl::GL_NEAREST; + texParams.name = "Blank AO"; + texParams.texels = &blankAO; + m_blankAO = new Ra::Engine::Data::Texture( texParams ); + m_blankAO->initializeGL(); + + m_nodeState = new globjects::State( globjects::State::DeferredMode ); + m_nodeState->enable( gl::GL_DEPTH_TEST ); + m_nodeState->depthFunc( gl::GL_LEQUAL ); + m_nodeState->depthMask( gl::GL_FALSE ); + m_nodeState->colorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + m_nodeState->blendFuncSeparate( gl::GL_ONE, gl::GL_DST_ALPHA, gl::GL_ONE, gl::GL_ZERO ); + m_nodeState->enable( gl::GL_BLEND ); +} + +void LocalLightingNode::destroy() { + delete m_framebuffer; + delete m_blankAO; + + delete m_nodeState; +} + +void LocalLightingNode::resize( uint32_t, uint32_t ) {} + +void LocalLightingNode::buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const { + + auto mat = const_cast( ro )->getMaterial(); + // Volumes are not used in EnvLightPass + if ( mat->getMaterialAspect() == Ra::Engine::Data::Material::MaterialAspect::MAT_DENSITY ) { + return; + } + + if ( auto cfg = Ra::Engine::Data::ShaderConfigurationFactory::getConfiguration( + { "LocalLightNode::" + mat->getMaterialName() } ) ) { + rt.setConfiguration( *cfg, m_idx ); + } + else { + std::string resourcesRootDir = m_resourceDir + "Shaders/LocalLightNode/"; + // Build the shader configuration + Ra::Engine::Data::ShaderConfiguration theConfig { + { "LocalLightNode::" + mat->getMaterialName() }, + m_resourceDir + "Shaders/shader.vert.glsl", + resourcesRootDir + "shader.frag.glsl" }; + // add the material interface to the fragment shader + theConfig.addInclude( "\"" + mat->getMaterialName() + ".glsl\"", + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + // Add to the ShaderConfigManager + Ra::Engine::Data::ShaderConfigurationFactory::addConfiguration( theConfig ); + // Add to the RenderTechnique + rt.setConfiguration( theConfig, m_idx ); + } + rt.setParametersProvider( mat, m_idx ); +} + +bool LocalLightingNode::execute() { + // Get parameters + // Render objects + auto& renderObjects = m_inObjects->getData(); + // Cameras + auto& camera = m_inCamera->getData(); + // Lights + auto& lights = m_inLights->getData(); + + // Color tex + m_colorTexture = &m_inColor->getData(); + m_outColor->setData( m_colorTexture ); + + // Depth buffer + auto& depthBuffer = m_inDepth->getData(); + + // Ambiant occlusion buffer + auto aoTexture = m_inAo->isLinked() ? &m_inAo->getData() : m_blankAO; + + // Compute the result + m_framebuffer->bind(); + m_framebuffer->attachTexture( gl::GL_DEPTH_ATTACHMENT, depthBuffer.texture() ); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_colorTexture->texture() ); + + const gl::GLenum buffers[] = { gl::GL_COLOR_ATTACHMENT0 }; + gl::glDrawBuffers( 1, buffers ); + + auto currentState = globjects::State::currentState(); + m_nodeState->apply(); + + if ( lights.size() > 0 ) { + for ( const auto& l : lights ) { + Ra::Engine::Data::RenderParameters inPassParams; + // Ambient occlusion is not really meaningful for local lighting + inPassParams.addParameter( "amb_occ_sampler", aoTexture ); + l->getRenderParameters( inPassParams ); + for ( const auto& ro : renderObjects ) { + ro->render( inPassParams, camera, m_idx ); + } + } + } + currentState->apply(); + m_framebuffer->unbind(); + // Set output port + // here, they are already set by constructor + return true; +} + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/LocalLightingNode.hpp b/src/Dataflow/Rendering/Nodes/RenderNodes/LocalLightingNode.hpp new file mode 100644 index 00000000000..a5f73a2de94 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/LocalLightingNode.hpp @@ -0,0 +1,64 @@ +#pragma once +#include + +#include + +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { +using namespace Ra::Dataflow::Core; +/** + * \brief Simple render node : render only opaque objects with direct lighting. + * + */ +class RA_DATAFLOW_API LocalLightingNode : public RenderingNode +{ + public: + explicit LocalLightingNode( const std::string& name ); + + void init() override; + + bool execute() override; + + void destroy() override; + + void resize( uint32_t width, uint32_t height ) override; + + void buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const override; + + bool hasRenderTechnique() override { return true; } + + static const std::string getTypename() { return "Local Lighting Node"; } + + private: + Ra::Engine::Data::Texture* m_colorTexture { nullptr }; + + Ra::Engine::Data::Texture* m_blankAO { nullptr }; + globjects::Framebuffer* m_framebuffer { nullptr }; + + globjects::State* m_nodeState; + + PortIn* m_inColor { new PortIn( "color", this ) }; + PortIn* m_inDepth { new PortIn( "depth", this ) }; + PortIn* m_inAo { new PortIn( "AO", this ) }; + + PortIn>* m_inObjects { + new PortIn>( "objects", this ) }; + PortIn>* m_inLights { + new PortIn>( "lights", this ) }; + PortIn* m_inCamera { new PortIn( "camera", this ) }; + + PortOut* m_outColor { new PortOut( "Beauty", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/SimpleRenderNode.cpp b/src/Dataflow/Rendering/Nodes/RenderNodes/SimpleRenderNode.cpp new file mode 100644 index 00000000000..796ef48ad9b --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/SimpleRenderNode.cpp @@ -0,0 +1,187 @@ +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Engine::Data; + +SimpleRenderNode::SimpleRenderNode( const std::string& name ) : + RenderingNode( name, getTypename() ) { + addInput( m_inObjects ); + m_inObjects->mustBeLinked(); + addInput( m_inLights ); + m_inLights->mustBeLinked(); + addInput( m_inCamera ); + m_inCamera->mustBeLinked(); + + Ra::Engine::Data::TextureParameters colorTexParams = { name, + gl::GL_TEXTURE_2D, + 1, + 1, + 1, + gl::GL_RGBA, + gl::GL_RGBA32F, + gl::GL_FLOAT, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_LINEAR, + gl::GL_LINEAR, + nullptr }; + m_colorTexture = new Ra::Engine::Data::Texture( colorTexParams ); + addOutput( m_outColorTex, m_colorTexture ); + + Ra::Engine::Data::TextureParameters depthTexParams = { "Simple Depth image", + gl::GL_TEXTURE_2D, + 1, + 1, + 1, + gl::GL_DEPTH_COMPONENT, + gl::GL_DEPTH_COMPONENT24, + gl::GL_UNSIGNED_INT, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_NEAREST, + gl::GL_NEAREST, + nullptr }; + m_depthTexture = new Ra::Engine::Data::Texture( depthTexParams ); + addOutput( m_outDepthTex, m_depthTexture ); +} + +void SimpleRenderNode::init() { + m_framebuffer = new globjects::Framebuffer(); + + float blankAO[4] = { 1.f, 1.f, 1.f, 1.f }; + Ra::Engine::Data::TextureParameters texParams; + texParams.target = gl::GL_TEXTURE_2D; + texParams.width = 1; + texParams.height = 1; + texParams.internalFormat = gl::GL_RGBA32F; + texParams.format = gl::GL_RGBA; + texParams.type = gl::GL_FLOAT; + texParams.minFilter = gl::GL_NEAREST; + texParams.magFilter = gl::GL_NEAREST; + texParams.name = "Blank AO"; + texParams.texels = &blankAO; + m_blankAO = new Ra::Engine::Data::Texture( texParams ); + m_blankAO->initializeGL(); + + m_nodeState = new globjects::State( globjects::State::DeferredMode ); + m_nodeState->enable( gl::GL_DEPTH_TEST ); + m_nodeState->depthMask( gl::GL_TRUE ); + m_nodeState->depthFunc( gl::GL_LEQUAL ); + m_nodeState->colorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + m_nodeState->blendFunc( gl::GL_ONE, gl::GL_ONE ); + m_nodeState->disable( gl::GL_BLEND ); +} + +void SimpleRenderNode::destroy() { + delete m_framebuffer; + delete m_colorTexture; + delete m_depthTexture; + delete m_blankAO; + + delete m_nodeState; +} + +void SimpleRenderNode::resize( uint32_t width, uint32_t height ) { + m_colorTexture->resize( width, height ); + m_depthTexture->resize( width, height ); + m_framebuffer->bind(); + m_framebuffer->attachTexture( gl::GL_DEPTH_ATTACHMENT, m_depthTexture->texture() ); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_colorTexture->texture() ); + m_framebuffer->unbind(); +} + +void SimpleRenderNode::buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const { + + auto mat = const_cast( ro )->getMaterial(); + // Volumes are not used in EnvLightPass + if ( mat->getMaterialAspect() == Ra::Engine::Data::Material::MaterialAspect::MAT_DENSITY ) { + return; + } + + if ( auto cfg = Ra::Engine::Data::ShaderConfigurationFactory::getConfiguration( + { "LocalLightNode::" + mat->getMaterialName() } ) ) { + rt.setConfiguration( *cfg, m_idx ); + } + else { + std::string resourcesRootDir = m_resourceDir + "Shaders/LocalLightNode/"; + // Build the shader configuration + Ra::Engine::Data::ShaderConfiguration theConfig { + { "LocalLightNode::" + mat->getMaterialName() }, + m_resourceDir + "Shaders/shader.vert.glsl", + resourcesRootDir + "shader.frag.glsl" }; + // add the material interface to the fragment shader + theConfig.addInclude( "\"" + mat->getMaterialName() + ".glsl\"", + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + // Add to the ShaderConfigManager + Ra::Engine::Data::ShaderConfigurationFactory::addConfiguration( theConfig ); + // Add to the RenderTechnique + rt.setConfiguration( theConfig, m_idx ); + } + rt.setParametersProvider( mat, m_idx ); +} + +bool SimpleRenderNode::execute() { + // Get parameters + // Render objects + auto& renderObjects = m_inObjects->getData(); + // Cameras + auto& camera = m_inCamera->getData(); + // Lights + auto& lights = m_inLights->getData(); + + // Compute the result + m_framebuffer->bind(); + + const gl::GLenum buffers[] = { gl::GL_COLOR_ATTACHMENT0 }; + gl::glDrawBuffers( 1, buffers ); + + auto currentState = globjects::State::currentState(); + m_nodeState->apply(); + + float clearColor[4] = { 0.02f, 0.03f, 0.04f, 0.0f }; + float clearDepth = 1.0f; + gl::glClearBufferfv( gl::GL_COLOR, 0, clearColor ); + gl::glClearBufferfv( gl::GL_DEPTH, 0, &clearDepth ); + + if ( lights.size() > 0 ) { + bool first_light = true; + for ( const auto& l : lights ) { + Ra::Engine::Data::RenderParameters inPassParams; + inPassParams.addParameter( "amb_occ_sampler", m_blankAO ); + l->getRenderParameters( inPassParams ); + for ( const auto& ro : renderObjects ) { + ro->render( inPassParams, camera, m_idx ); + } + if ( first_light ) { + // break; + first_light = false; + gl::glEnable( gl::GL_BLEND ); + gl::glBlendFunc( gl::GL_ONE, gl::GL_ONE ); + gl::glDepthMask( gl::GL_FALSE ); + } + } + } + + currentState->apply(); + + m_framebuffer->unbind(); + + // Set output port + // here, they are already set by constructor + return true; +} + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/SimpleRenderNode.hpp b/src/Dataflow/Rendering/Nodes/RenderNodes/SimpleRenderNode.hpp new file mode 100644 index 00000000000..c7f8d0a38d6 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/SimpleRenderNode.hpp @@ -0,0 +1,63 @@ +#pragma once +#include + +#include + +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { +using namespace Ra::Dataflow::Core; +/** + * \brief Simple render node : render only opaque objects with direct lighting. + * + */ +class RA_DATAFLOW_API SimpleRenderNode : public RenderingNode +{ + public: + explicit SimpleRenderNode( const std::string& name ); + + void init() override; + + bool execute() override; + + void destroy() override; + + void resize( uint32_t width, uint32_t height ) override; + + void buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const override; + + bool hasRenderTechnique() override { return true; } + + static const std::string getTypename() { return "Simple Render Node"; } + + private: + Ra::Engine::Data::Texture* m_colorTexture { nullptr }; + Ra::Engine::Data::Texture* m_depthTexture { nullptr }; + + Ra::Engine::Data::Texture* m_blankAO { nullptr }; + + globjects::Framebuffer* m_framebuffer { nullptr }; + + globjects::State* m_nodeState; + + PortIn>* m_inObjects { + new PortIn>( "objects", this ) }; + PortIn>* m_inLights { + new PortIn>( "lights", this ) }; + PortIn* m_inCamera { new PortIn( "camera", this ) }; + + PortOut* m_outColorTex { new PortOut( "Beauty", this ) }; + PortOut* m_outDepthTex { new PortOut( "Depth AOV", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/SsaoRenderNode.cpp b/src/Dataflow/Rendering/Nodes/RenderNodes/SsaoRenderNode.cpp new file mode 100644 index 00000000000..343c0c7ee85 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/SsaoRenderNode.cpp @@ -0,0 +1,231 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Engine::Data; + +SsaoNode::SsaoNode( const std::string& name ) : RenderingNode( name, getTypename() ) { + + addInput( m_inWorldNormal ); + m_inWorldNormal->mustBeLinked(); + addInput( m_inWorldPos ); + m_inWorldPos->mustBeLinked(); + addInput( m_inCamera ); + m_inCamera->mustBeLinked(); + + addInput( m_aoRadius ); + addInput( m_aoSamples ); + + Ra::Engine::Data::TextureParameters texParams; + texParams.target = gl::GL_TEXTURE_2D; + texParams.minFilter = gl::GL_LINEAR; + texParams.magFilter = gl::GL_LINEAR; + texParams.internalFormat = gl::GL_RGBA32F; + texParams.format = gl::GL_RGBA; + texParams.type = gl::GL_FLOAT; + texParams.name = "ssao"; + m_AO = new Ra::Engine::Data::Texture( texParams ); + + addOutput( m_ssao, m_AO ); + + auto editableRadius = new EditableParameter( "radius", m_editableAORadius ); + nlohmann::json radiusConstraints = { { "min", 0. }, { "max", 100. } }; + editableRadius->setConstraints( radiusConstraints ); + addEditableParameter( editableRadius ); + + auto editableSamples = new EditableParameter( "samples", m_editableSamples ); + nlohmann::json sampleConstraints = { { "min", 0 }, { "max", 4096 } }; + editableSamples->setConstraints( sampleConstraints ); + addEditableParameter( editableSamples ); +} + +void SsaoNode::init() { + Ra::Engine::Data::TextureParameters texParams; + texParams.target = gl::GL_TEXTURE_2D; + texParams.minFilter = gl::GL_LINEAR; + texParams.magFilter = gl::GL_LINEAR; + texParams.internalFormat = gl::GL_RGBA32F; + texParams.format = gl::GL_RGBA; + texParams.type = gl::GL_FLOAT; + texParams.name = "Raw Ambient Occlusion"; + m_rawAO = new Ra::Engine::Data::Texture( texParams ); + + Ra::Core::Geometry::TriangleMesh mesh = + Ra::Core::Geometry::makeZNormalQuad( Ra::Core::Vector2( -1.f, 1.f ) ); + auto qm = std::make_unique( "caller" ); + qm->loadGeometry( std::move( mesh ) ); + m_quadMesh = std::move( qm ); + m_quadMesh->updateGL(); + + // TODO make the sampling method an editable parameter + m_sphereSampler = + std::make_unique( SphereSampler::SamplingMethod::HAMMERSLEY, 64 ); + + m_blurFramebuffer = new globjects::Framebuffer(); + m_framebuffer = new globjects::Framebuffer(); +} + +void SsaoNode::destroy() { + delete m_rawAO; + delete m_AO; + delete m_blurFramebuffer; + delete m_framebuffer; +} + +void SsaoNode::resize( uint32_t width, uint32_t height ) { + m_rawAO->resize( width, height ); + m_AO->resize( width, height ); + + m_framebuffer->bind(); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_rawAO->texture() ); + + m_blurFramebuffer->bind(); + m_blurFramebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_AO->texture() ); + + globjects::Framebuffer::unbind(); +} + +bool SsaoNode::execute() { + auto aabb = Ra::Engine::RadiumEngine::getInstance()->computeSceneAabb(); + if ( aabb.isEmpty() ) { m_sceneDiag = 1_ra; } + else { m_sceneDiag = aabb.diagonal().norm(); } + + Ra::Engine::Data::RenderParameters inPassParams; + // Positions + auto posTexture = &m_inWorldPos->getData(); + // Normals + auto normalTexture = &m_inWorldNormal->getData(); + + // AO Radius + auto aoRadius = m_editableAORadius; + if ( m_aoRadius->isLinked() ) { aoRadius = m_aoRadius->getData(); } + + // AO Samples + auto samples = m_editableSamples; + if ( m_aoSamples->isLinked() ) { samples = m_aoSamples->getData(); } + if ( m_currentSamples != samples ) { + m_currentSamples = samples; + m_sphereSampler = std::make_unique( + SphereSampler::SamplingMethod::HAMMERSLEY, m_currentSamples ); + } + + // Cameras + auto& camera = m_inCamera->getData(); + + m_framebuffer->bind(); + const gl::GLenum buffers[] = { gl::GL_COLOR_ATTACHMENT0 }; + gl::glDrawBuffers( 1, buffers ); + float clearWhite[4] = { 1.0f, 1.0f, 1.0f, 0.0f }; + gl::glClearBufferfv( gl::GL_COLOR, 0, clearWhite ); + gl::glDisable( gl::GL_DEPTH_TEST ); + gl::glDepthMask( gl::GL_FALSE ); + + m_shader->bind(); + Ra::Core::Matrix4 viewProj = camera.projMatrix * camera.viewMatrix; + + m_shader->setUniform( "transform.mvp", viewProj ); + m_shader->setUniform( "transform.proj", camera.projMatrix ); + m_shader->setUniform( "transform.view", camera.viewMatrix ); + + m_shader->setUniform( "normal_sampler", normalTexture, 0 ); + m_shader->setUniform( "position_sampler", posTexture, 1 ); + m_shader->setUniform( "dir_sampler", m_sphereSampler->asTexture(), 2 ); + m_shader->setUniform( "ssdoRadius", aoRadius / 100_ra * m_sceneDiag ); + + m_quadMesh->render( m_shader ); + gl::glEnable( gl::GL_DEPTH_TEST ); + gl::glDepthMask( gl::GL_TRUE ); + m_framebuffer->unbind(); + + m_blurFramebuffer->bind(); + m_blurShader->bind(); + gl::glDrawBuffers( 1, buffers ); + gl::glClearBufferfv( gl::GL_COLOR, 0, clearWhite ); + gl::glDisable( gl::GL_DEPTH_TEST ); + gl::glDepthMask( gl::GL_FALSE ); + + m_shader->setUniform( "transform.mvp", viewProj ); + m_shader->setUniform( "transform.proj", camera.projMatrix ); + m_shader->setUniform( "transform.view", camera.viewMatrix ); + m_shader->setUniform( "ao_sampler", m_rawAO, 0 ); + + m_quadMesh->render( m_shader ); + gl::glEnable( gl::GL_DEPTH_TEST ); + gl::glDepthMask( gl::GL_TRUE ); + m_blurFramebuffer->unbind(); + + return true; +} + +void SsaoNode::toJsonInternal( nlohmann::json& data ) const { + if ( m_currentSamples != AO_DefaultSamples ) { + // do not write default value + data["samples"] = m_currentSamples; + } + if ( m_editableAORadius != AO_DefaultRadius ) { + // do not write default value + data["radius"] = m_editableAORadius; + } +} + +bool SsaoNode::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "radius" ) ) { m_editableAORadius = data["radius"]; } + if ( data.contains( "samples" ) ) { + m_currentSamples = data["samples"]; + m_editableSamples = Scalar( m_currentSamples ); + } + return true; +} + +bool SsaoNode::initInternalShaders() { + if ( !m_hasShaders ) { + const std::string vertexShaderSource { "layout (location = 0) in vec3 in_position;\n" + "out vec2 varTexcoord;\n" + "void main(void) {\n" + " gl_Position = vec4(in_position.xyz, 1.0);\n" + " varTexcoord = (in_position.xy + 1.0) / 2.0;\n" + "}" }; + std::string resourcesRootDir = m_resourceDir + "Shaders/SsaoNode/"; + + Ra::Engine::Data::ShaderConfiguration ssdoConfig { "SSDO" }; + ssdoConfig.addShaderSource( Ra::Engine::Data::ShaderType::ShaderType_VERTEX, + vertexShaderSource ); + ssdoConfig.addShader( Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT, + resourcesRootDir + "ssao.frag.glsl" ); + auto added = m_shaderMngr->addShaderProgram( ssdoConfig ); + if ( added ) { m_shader = added.value(); } + else { + std::cout << "AO: Could not add shader." << std::endl; + return false; + } + + Ra::Engine::Data::ShaderConfiguration blurssdoConfig { "blurSSDO" }; + blurssdoConfig.addShaderSource( Ra::Engine::Data::ShaderType::ShaderType_VERTEX, + vertexShaderSource ); + blurssdoConfig.addShader( Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT, + resourcesRootDir + "blurao.frag.glsl" ); + added = m_shaderMngr->addShaderProgram( blurssdoConfig ); + if ( added ) { m_blurShader = added.value(); } + else { + std::cout << "AO: Could not add shader." << std::endl; + return false; + } + m_hasShaders = true; + } + return m_hasShaders; +} + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/SsaoRenderNode.hpp b/src/Dataflow/Rendering/Nodes/RenderNodes/SsaoRenderNode.hpp new file mode 100644 index 00000000000..f673e11f073 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/SsaoRenderNode.hpp @@ -0,0 +1,76 @@ +#pragma once +#include + +#include + +#include +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { +using namespace Ra::Dataflow::Core; +using namespace Ra::Engine::Data; + +/** + * \brief compute ssao in image space from world space geometry buffer. + * + */ +class RA_DATAFLOW_API SsaoNode : public RenderingNode +{ + public: + explicit SsaoNode( const std::string& name ); + + void init() override; + bool execute() override; + void destroy() override; + void resize( uint32_t width, uint32_t height ) override; + bool initInternalShaders() override; + + static const std::string getTypename() { return "SSAO Node"; } + + protected: + void toJsonInternal( nlohmann::json& data ) const override; + bool fromJsonInternal( const nlohmann::json& data ) override; + + private: + bool m_hasShaders { false }; + + static constexpr Scalar AO_DefaultRadius { 5_ra }; + static constexpr int AO_DefaultSamples { 64 }; + + TextureType* m_rawAO { nullptr }; + TextureType* m_AO { nullptr }; + + Scalar m_editableAORadius { AO_DefaultRadius }; + int m_editableSamples { AO_DefaultSamples }; + + int m_currentSamples { AO_DefaultSamples }; + Scalar m_sceneDiag { 1.0 }; + + std::unique_ptr m_quadMesh { nullptr }; + std::unique_ptr m_sphereSampler { nullptr }; + + const Ra::Engine::Data::ShaderProgram* m_shader { nullptr }; + const Ra::Engine::Data::ShaderProgram* m_blurShader { nullptr }; + + globjects::Framebuffer* m_blurFramebuffer { nullptr }; + globjects::Framebuffer* m_framebuffer { nullptr }; + + PortIn* m_inWorldPos { new PortIn( "worldPosition", this ) }; + PortIn* m_inWorldNormal { new PortIn( "worldNormal", this ) }; + PortIn* m_inCamera { new PortIn( "camera", this ) }; + + PortIn* m_aoRadius { new PortIn( "radius", this ) }; + PortIn* m_aoSamples { new PortIn( "samples", this ) }; + + PortOut* m_ssao { new PortOut( "ssao", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/TransparentLocalLightingNode.cpp b/src/Dataflow/Rendering/Nodes/RenderNodes/TransparentLocalLightingNode.cpp new file mode 100644 index 00000000000..bc895f3fab2 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/TransparentLocalLightingNode.cpp @@ -0,0 +1,234 @@ +#include + +#include +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Engine::Data; + +TransparentLocalLightingNode::TransparentLocalLightingNode( const std::string& name ) : + RenderingNode( name, getTypename() ) { + + addInput( m_inObjects ); + m_inObjects->mustBeLinked(); + addInput( m_inLights ); + m_inLights->mustBeLinked(); + addInput( m_inCamera ); + m_inCamera->mustBeLinked(); + + // TODO, allow to not provide the two following inputs, then using internal textures ? + addInput( m_inColor ); + m_inColor->mustBeLinked(); + addInput( m_inDepth ); + m_inDepth->mustBeLinked(); + + addOutput( m_outColor, m_colorTexture ); + addOutput( m_outRevealage, m_revealageTexture ); + addOutput( m_outAccumulation, m_accumulationTexture ); +} + +void TransparentLocalLightingNode::init() { + m_framebuffer = new globjects::Framebuffer(); + m_oitFramebuffer = new globjects::Framebuffer(); + + Ra::Engine::Data::TextureParameters texParams; + texParams.target = gl::GL_TEXTURE_2D; + texParams.minFilter = gl::GL_LINEAR; + texParams.magFilter = gl::GL_LINEAR; + texParams.internalFormat = gl::GL_RGBA32F; + texParams.format = gl::GL_RGBA; + texParams.type = gl::GL_FLOAT; + texParams.name = "Accumulation"; + m_accumulationTexture = new Ra::Engine::Data::Texture( texParams ); + texParams.name = "Revealage"; + m_revealageTexture = new Ra::Engine::Data::Texture( texParams ); + + m_outRevealage->setData( m_revealageTexture ); + m_outAccumulation->setData( m_accumulationTexture ); + + m_nodeState = new globjects::State( globjects::State::DeferredMode ); + m_nodeState->enable( gl::GL_DEPTH_TEST ); + m_nodeState->depthFunc( gl::GL_LEQUAL ); + m_nodeState->depthMask( gl::GL_FALSE ); + m_nodeState->colorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + // no blendEquation in globjects::State + // m_nodeState->blendEquation(gl::GL_FUNC_ADD); + // no blendFunci in globjects::State + // m_nodeState->blendFunci( 0, gl::GL_ONE, gl::GL_ONE ); + // m_nodeState->blendFunci( 1, gl::GL_ZERO, gl::GL_ONE_MINUS_SRC_ALPHA ); + m_nodeState->enable( gl::GL_BLEND ); + + m_composeState = new globjects::State( globjects::State::DeferredMode ); + m_composeState->disable( gl::GL_DEPTH_TEST ); + m_composeState->depthMask( gl::GL_FALSE ); + m_composeState->colorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + m_composeState->blendFunc( gl::GL_ONE_MINUS_SRC_ALPHA, gl::GL_SRC_ALPHA ); + m_composeState->enable( gl::GL_BLEND ); +} + +bool TransparentLocalLightingNode::initInternalShaders() { + if ( !m_hasShaders ) { + Ra::Core::Geometry::TriangleMesh mesh = + Ra::Core::Geometry::makeZNormalQuad( Ra::Core::Vector2( -1.f, 1.f ) ); + auto qm = std::make_unique( "caller" ); + qm->loadGeometry( std::move( mesh ) ); + m_quadMesh = std::move( qm ); + m_quadMesh->updateGL(); + + const std::string composeVertexShader { "layout (location = 0) in vec3 in_position;\n" + "out vec2 varTexcoord;\n" + "void main()\n" + "{\n" + " gl_Position = vec4(in_position, 1.0);\n" + " varTexcoord = (in_position.xy + 1.0) * 0.5;\n" + "}\n" }; + const std::string composeFragmentShader { + "in vec2 varTexcoord;\n" + "out vec4 f_Color;\n" + "uniform sampler2D u_OITSumColor;\n" + "uniform sampler2D u_OITSumWeight;\n" + "void main() {\n" + " float r = texture( u_OITSumWeight, varTexcoord ).r;\n" + " if ( r >= 1.0 ) { discard; }\n" + " vec4 accum = texture( u_OITSumColor, varTexcoord );\n" + " vec3 avg_color = accum.rgb / max( accum.a, 0.00001 );\n" + " f_Color = vec4( avg_color, r );\n" + "}" }; + + Ra::Engine::Data::ShaderConfiguration config { "TransparencyNode::ComposeTransparency" }; + config.addShaderSource( Ra::Engine::Data::ShaderType::ShaderType_VERTEX, + composeVertexShader ); + config.addShaderSource( Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT, + composeFragmentShader ); + if ( m_shader == nullptr ) { + auto added = m_shaderMngr->addShaderProgram( config ); + if ( added ) { + m_shader = added.value(); + m_hasShaders = true; + } + } + } + return m_hasShaders; +} +void TransparentLocalLightingNode::destroy() { + delete m_framebuffer; + delete m_oitFramebuffer; + delete m_accumulationTexture; + delete m_revealageTexture; + + delete m_nodeState; + delete m_composeState; +} + +void TransparentLocalLightingNode::resize( uint32_t width, uint32_t height ) { + m_accumulationTexture->resize( width, height ); + m_revealageTexture->resize( width, height ); + m_oitFramebuffer->bind(); + m_oitFramebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_accumulationTexture->texture() ); + m_oitFramebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT1, m_revealageTexture->texture() ); + m_oitFramebuffer->unbind(); +} + +void TransparentLocalLightingNode::buildRenderTechnique( + const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const { + + auto mat = const_cast( ro )->getMaterial(); + // Volumes are not used in EnvLightPass + if ( mat->getMaterialAspect() == Ra::Engine::Data::Material::MaterialAspect::MAT_DENSITY ) { + return; + } + + if ( auto cfg = Ra::Engine::Data::ShaderConfigurationFactory::getConfiguration( + { "TransparencyLocalLightNode::" + mat->getMaterialName() } ) ) { + rt.setConfiguration( *cfg, m_idx ); + } + else { + std::string resourcesRootDir = m_resourceDir + "Shaders/TransparencyLocalLightNode/"; + // Build the shader configuration + Ra::Engine::Data::ShaderConfiguration theConfig { + { "TransparencyLocalLightNode::" + mat->getMaterialName() }, + m_resourceDir + "Shaders/shader.vert.glsl", + resourcesRootDir + "shader.frag.glsl" }; + // add the material interface to the fragment shader + theConfig.addInclude( "\"" + mat->getMaterialName() + ".glsl\"", + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + // Add to the ShaderConfigManager + Ra::Engine::Data::ShaderConfigurationFactory::addConfiguration( theConfig ); + // Add to the RenderTechnique + rt.setConfiguration( theConfig, m_idx ); + } + rt.setParametersProvider( mat, m_idx ); +} + +bool TransparentLocalLightingNode::execute() { + // Get parameters + // Render objects + auto& renderObjects = m_inObjects->getData(); + // Cameras + auto& camera = m_inCamera->getData(); + // Lights + auto& lights = m_inLights->getData(); + + // Color tex + m_colorTexture = &m_inColor->getData(); + m_outColor->setData( m_colorTexture ); + + // Depth buffer + auto& depthBuffer = m_inDepth->getData(); + + auto currentState = globjects::State::currentState(); + m_nodeState->apply(); + Ra::Engine::Data::RenderParameters inPassParams; + + // Render transparent objects + static const float clearZeros[4] = { 0.0, 0.0, 0.0, 1.0 }; + static const float clearOnes[4] = { 1.0, 1.0, 1.0, 1.0 }; + const gl::GLenum buffers[] = { gl::GL_COLOR_ATTACHMENT0, gl::GL_COLOR_ATTACHMENT1 }; + m_oitFramebuffer->bind(); + m_oitFramebuffer->attachTexture( gl::GL_DEPTH_ATTACHMENT, depthBuffer.texture() ); + gl::glDrawBuffers( 2, buffers ); + gl::glClearBufferfv( gl::GL_COLOR, 0, clearZeros ); + gl::glClearBufferfv( gl::GL_COLOR, 1, clearOnes ); + gl::glBlendEquation( gl::GL_FUNC_ADD ); + gl::glBlendFunci( 0, gl::GL_ONE, gl::GL_ONE ); + gl::glBlendFunci( 1, gl::GL_ZERO, gl::GL_ONE_MINUS_SRC_ALPHA ); + if ( lights.size() > 0 ) { + for ( const auto& l : lights ) { + l->getRenderParameters( inPassParams ); + for ( const auto& ro : renderObjects ) { + ro->render( inPassParams, camera, m_idx ); + } + } + } + m_oitFramebuffer->detach( gl::GL_DEPTH_ATTACHMENT ); + + // Compute the result + m_framebuffer->bind(); + m_framebuffer->attachTexture( gl::GL_DEPTH_ATTACHMENT, depthBuffer.texture() ); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_colorTexture->texture() ); + gl::glDrawBuffers( 1, buffers ); + m_composeState->apply(); + + m_shader->bind(); + m_shader->setUniform( "u_OITSumColor", m_accumulationTexture, 0 ); + m_shader->setUniform( "u_OITSumWeight", m_revealageTexture, 1 ); + m_quadMesh->render( m_shader ); + + currentState->apply(); + m_framebuffer->unbind(); + // Set output port + // here, they are already set by constructor + return true; +} + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/TransparentLocalLightingNode.hpp b/src/Dataflow/Rendering/Nodes/RenderNodes/TransparentLocalLightingNode.hpp new file mode 100644 index 00000000000..7c2bb2669e3 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/TransparentLocalLightingNode.hpp @@ -0,0 +1,83 @@ +#pragma once +#include + +#include + +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { +using namespace Ra::Dataflow::Core; +/** + * \brief Simple render node : render only opaque objects with direct lighting. + * + */ +class RA_DATAFLOW_API TransparentLocalLightingNode : public RenderingNode +{ + public: + explicit TransparentLocalLightingNode( const std::string& name ); + + void init() override; + + bool execute() override; + + void destroy() override; + + void resize( uint32_t width, uint32_t height ) override; + + bool initInternalShaders() override; + + void buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const override; + + bool hasRenderTechnique() override { return true; } + + static const std::string getTypename() { return "Transparency Local Lighting Node"; } + + private: + bool m_hasShaders { false }; + /// The accumulation texture + Ra::Engine::Data::Texture* m_accumulationTexture { nullptr }; + + /// The revealage texture + Ra::Engine::Data::Texture* m_revealageTexture { nullptr }; + + /// The framebuffer used to draw the transparent objects + globjects::Framebuffer* m_oitFramebuffer { nullptr }; + + /// The composite shader, owned by the shader manager + const Ra::Engine::Data::ShaderProgram* m_shader { nullptr }; + + /// The fullscreen quad to draw + std::unique_ptr m_quadMesh { nullptr }; + + Ra::Engine::Data::Texture* m_colorTexture { nullptr }; + + globjects::Framebuffer* m_framebuffer { nullptr }; + + globjects::State* m_nodeState; + globjects::State* m_composeState; + + PortIn* m_inColor { new PortIn( "color", this ) }; + PortIn* m_inDepth { new PortIn( "depth", this ) }; + + PortIn>* m_inObjects { + new PortIn>( "objects", this ) }; + PortIn>* m_inLights { + new PortIn>( "lights", this ) }; + PortIn* m_inCamera { new PortIn( "camera", this ) }; + + PortOut* m_outColor { new PortOut( "Beauty", this ) }; + PortOut* m_outRevealage { new PortOut( "revealage", this ) }; + PortOut* m_outAccumulation { new PortOut( "accum", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/VolumeLocalLightingNode.cpp b/src/Dataflow/Rendering/Nodes/RenderNodes/VolumeLocalLightingNode.cpp new file mode 100644 index 00000000000..82e4a2f917c --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/VolumeLocalLightingNode.cpp @@ -0,0 +1,197 @@ +#include + +#include +#include +#include +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Engine::Data; + +VolumeLocalLightingNode::VolumeLocalLightingNode( const std::string& name ) : + RenderingNode( name, getTypename() ) { + + addInput( m_inObjects ); + m_inObjects->mustBeLinked(); + addInput( m_inLights ); + m_inLights->mustBeLinked(); + addInput( m_inCamera ); + m_inCamera->mustBeLinked(); + + // TODO, allow to not provide the two following inputs, then using internal textures ? + addInput( m_inColor ); + m_inColor->mustBeLinked(); + addInput( m_inDepth ); + m_inDepth->mustBeLinked(); + + addOutput( m_outColor, m_colorTexture ); +} + +void VolumeLocalLightingNode::init() { + m_framebuffer = new globjects::Framebuffer(); + m_volumeFramebuffer = new globjects::Framebuffer(); + + Ra::Engine::Data::TextureParameters texParams; + texParams.target = gl::GL_TEXTURE_2D; + texParams.minFilter = gl::GL_LINEAR; + texParams.magFilter = gl::GL_LINEAR; + texParams.internalFormat = gl::GL_RGBA32F; + texParams.format = gl::GL_RGBA; + texParams.type = gl::GL_FLOAT; + texParams.name = "Volume"; + m_volumeTexture = new Ra::Engine::Data::Texture( texParams ); + + m_nodeState = new globjects::State( globjects::State::DeferredMode ); + m_nodeState->enable( gl::GL_DEPTH_TEST ); + m_nodeState->depthFunc( gl::GL_LEQUAL ); + m_nodeState->depthMask( gl::GL_FALSE ); + m_nodeState->colorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + m_nodeState->disable( gl::GL_BLEND ); + + m_composeState = new globjects::State( globjects::State::DeferredMode ); + m_composeState->disable( gl::GL_DEPTH_TEST ); + m_composeState->depthMask( gl::GL_FALSE ); + m_composeState->colorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + m_composeState->blendFunc( gl::GL_ONE_MINUS_SRC_ALPHA, gl::GL_SRC_ALPHA ); + m_composeState->enable( gl::GL_BLEND ); +} + +bool VolumeLocalLightingNode::initInternalShaders() { + if ( m_shader == nullptr ) { + Ra::Core::Geometry::TriangleMesh mesh = + Ra::Core::Geometry::makeZNormalQuad( Ra::Core::Vector2( -1.f, 1.f ) ); + auto qm = std::make_unique( "caller" ); + qm->loadGeometry( std::move( mesh ) ); + m_quadMesh = std::move( qm ); + m_quadMesh->updateGL(); + + const std::string vrtxSrc { "layout (location = 0) in vec3 in_position;\n" + "out vec2 varTexcoord;\n" + "void main()\n" + "{\n" + " gl_Position = vec4(in_position, 1.0);\n" + " varTexcoord = (in_position.xy + 1.0) / 2.0;\n" + "}\n" }; + const std::string frgSrc { + "out vec4 fragColor;\n" + "in vec2 varTexcoord;\n" + "uniform sampler2D volumeImage;\n" + "void main()\n" + "{\n" + " vec2 size = vec2(textureSize(volumeImage, 0));\n" + " vec4 volColor = texelFetch(volumeImage, ivec2(varTexcoord.xy * size), 0);\n" + " if (volColor.a < 1)\n" + " discard;\n" + " fragColor = vec4(volColor.rgb, 0);\n" + "}\n" }; + Ra::Engine::Data::ShaderConfiguration config { "ComposeVolume" }; + config.addShaderSource( Ra::Engine::Data::ShaderType::ShaderType_VERTEX, vrtxSrc ); + config.addShaderSource( Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT, frgSrc ); + auto added = m_shaderMngr->addShaderProgram( config ); + if ( added ) { + m_shader = added.value(); + m_hasShaders = true; + } + } + return m_shader != nullptr; +} +void VolumeLocalLightingNode::destroy() { + delete m_framebuffer; + delete m_volumeFramebuffer; + delete m_volumeTexture; + + delete m_nodeState; + delete m_composeState; +} + +void VolumeLocalLightingNode::resize( uint32_t width, uint32_t height ) { + m_volumeTexture->resize( width, height ); + + m_volumeFramebuffer->bind(); + m_volumeFramebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_volumeTexture->texture() ); + m_volumeFramebuffer->unbind(); +} + +void VolumeLocalLightingNode::buildRenderTechnique( + const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const { + auto mat = const_cast( ro )->getMaterial(); + // Only volumes are used by this pass + if ( mat->getMaterialAspect() != Ra::Engine::Data::Material::MaterialAspect::MAT_DENSITY ) { + return; + } + // use the standard Radium shaders + auto passconfig = + Ra::Engine::Data::ShaderConfigurationFactory::getConfiguration( "Volumetric" ); + rt.setConfiguration( *passconfig, m_idx ); + rt.setParametersProvider( mat, m_idx ); +} + +bool VolumeLocalLightingNode::execute() { + // Get parameters + // Render objects + auto& renderObjects = m_inObjects->getData(); + // Cameras + auto& camera = m_inCamera->getData(); + // Lights + auto& lights = m_inLights->getData(); + + // Color tex + m_colorTexture = &m_inColor->getData(); + m_outColor->setData( m_colorTexture ); + + // Depth buffer + auto& depthBuffer = m_inDepth->getData(); + + auto currentState = globjects::State::currentState(); + + // Render transparent objects + static const float clearZeros[4] = { 0.0, 0.0, 0.0, 0.0 }; + const gl::GLenum buffers[] = { gl::GL_COLOR_ATTACHMENT0 }; + + m_volumeFramebuffer->bind(); + m_nodeState->apply(); + m_volumeFramebuffer->attachTexture( gl::GL_DEPTH_ATTACHMENT, depthBuffer.texture() ); + + gl::glDrawBuffers( 1, buffers ); + gl::glClearBufferfv( gl::GL_COLOR, 0, clearZeros ); + Ra::Engine::Data::RenderParameters inPassParams; + inPassParams.addParameter( "imageColor", m_colorTexture ); + inPassParams.addParameter( "imageDepth", depthBuffer.texture() ); + if ( lights.size() > 0 ) { + for ( const auto& l : lights ) { + l->getRenderParameters( inPassParams ); + for ( const auto& ro : renderObjects ) { + ro->render( inPassParams, camera, m_idx ); + } + } + } + // m_volumeFramebuffer->detach( gl::GL_DEPTH_ATTACHMENT ); + + // Compute the result + m_framebuffer->bind(); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_colorTexture->texture() ); + gl::glDrawBuffers( 1, buffers ); + m_composeState->apply(); + + m_shader->bind(); + m_shader->setUniform( "volumeImage", m_volumeTexture, 0 ); + m_quadMesh->render( m_shader ); + + currentState->apply(); + m_framebuffer->unbind(); + // Set output port + // here, they are already set by constructor + return true; +} + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/VolumeLocalLightingNode.hpp b/src/Dataflow/Rendering/Nodes/RenderNodes/VolumeLocalLightingNode.hpp new file mode 100644 index 00000000000..e73c53bb94c --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/VolumeLocalLightingNode.hpp @@ -0,0 +1,79 @@ +#pragma once +#include + +#include + +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { +using namespace Ra::Dataflow::Core; +/** + * \brief Simple render node : render only opaque objects with direct lighting. + * + */ +class RA_DATAFLOW_API VolumeLocalLightingNode : public RenderingNode +{ + public: + explicit VolumeLocalLightingNode( const std::string& name ); + + void init() override; + + bool execute() override; + + void destroy() override; + + void resize( uint32_t width, uint32_t height ) override; + + bool initInternalShaders() override; + + void buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const override; + + bool hasRenderTechnique() override { return true; } + + static const std::string getTypename() { return "Volume Local Lighting Node"; } + + private: + bool m_hasShaders { false }; + + /// The texture to draw the volume on + Ra::Engine::Data::Texture* m_volumeTexture { nullptr }; + + /// The framebuffer used to draw the volume + globjects::Framebuffer* m_volumeFramebuffer { nullptr }; + + /// The composite shader, owned by the shader manager + const Ra::Engine::Data::ShaderProgram* m_shader { nullptr }; + + /// The fullscreen quad to draw + std::unique_ptr m_quadMesh { nullptr }; + + Ra::Engine::Data::Texture* m_colorTexture { nullptr }; + + globjects::Framebuffer* m_framebuffer { nullptr }; + + globjects::State* m_nodeState; + globjects::State* m_composeState; + + PortIn* m_inColor { new PortIn( "color", this ) }; + PortIn* m_inDepth { new PortIn( "depth", this ) }; + + PortIn>* m_inObjects { + new PortIn>( "objects", this ) }; + PortIn>* m_inLights { + new PortIn>( "lights", this ) }; + PortIn* m_inCamera { new PortIn( "camera", this ) }; + + PortOut* m_outColor { new PortOut( "Beauty", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/WireframeRenderingNode.cpp b/src/Dataflow/Rendering/Nodes/RenderNodes/WireframeRenderingNode.cpp new file mode 100644 index 00000000000..2bbd825e16d --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/WireframeRenderingNode.cpp @@ -0,0 +1,259 @@ + + +#include + +#include +#include +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Engine::Data; + +// Warning : this will be removed when index layers will be available on Radium +namespace WireframeNodeInternal { + +template +void computeIndices( Ra::Core::Geometry::LineMesh::IndexContainerType& indices, + IndexContainerType& other ) { + + for ( const auto& index : other ) { + auto s = index.size(); + for ( unsigned int i = 0; i < s; ++i ) { + int i1 = index[i]; + int i2 = index[( i + 1 ) % s]; + if ( i1 > i2 ) std::swap( i1, i2 ); + indices.emplace_back( i1, i2 ); + } + } + + std::sort( indices.begin(), + indices.end(), + []( const Ra::Core::Geometry::LineMesh::IndexType& a, + const Ra::Core::Geometry::LineMesh::IndexType& b ) { + return a[0] < b[0] || ( a[0] == b[0] && a[1] < b[1] ); + } ); + indices.erase( std::unique( indices.begin(), indices.end() ), indices.end() ); +} + +// store LineMesh and Core, define the observer functor to update data one core update for wireframe +// linemesh +template +class VerticesUpdater +{ + public: + VerticesUpdater( std::shared_ptr disp, CoreGeometry& core ) : + m_disp { disp }, m_core { core } {}; + + void operator()() { m_disp->getCoreGeometry().setVertices( m_core.vertices() ); } + std::shared_ptr m_disp; + CoreGeometry& m_core; +}; + +template +class IndicesUpdater +{ + public: + IndicesUpdater( std::shared_ptr disp, CoreGeometry& core ) : + m_disp { disp }, m_core { core } {}; + + void operator()() { + auto lineIndices = m_disp->getCoreGeometry().getIndicesWithLock(); + computeIndices( lineIndices, m_core.getIndices() ); + m_disp->getCoreGeometry().indicesUnlock(); + } + std::shared_ptr m_disp; + CoreGeometry& m_core; +}; + +// create a linemesh to draw wireframe given a core mesh +template +void setupLineMesh( std::shared_ptr& disp, CoreGeometry& core ) { + Ra::Core::Geometry::LineMesh lines; + Ra::Core::Geometry::LineMesh::IndexContainerType indices; + lines.setVertices( core.vertices() ); + computeIndices( indices, core.getIndices() ); + if ( indices.size() > 0 ) { + lines.setIndices( std::move( indices ) ); + disp = std::make_shared( std::string( "wireframe" ), + std::move( lines ) ); + disp->updateGL(); + // add observer + auto handle = core.template getAttribHandle( + Ra::Core::Geometry::getAttribName( Ra::Core::Geometry::VERTEX_POSITION ) ); + + core.vertexAttribs().getAttrib( handle ).attach( VerticesUpdater( disp, core ) ); + core.attach( IndicesUpdater( disp, core ) ); + } + else { disp.reset(); } +} + +template +void processLineMesh( const std::shared_ptr& m, + std::shared_ptr& r ) { + if ( m->getRenderMode() == + Ra::Engine::Data::AttribArrayDisplayable::MeshRenderMode::RM_TRIANGLES ) { + setupLineMesh( r, m->getCoreGeometry() ); + } +} + +} // namespace WireframeNodeInternal + +WireframeRenderingNode::WireframeRenderingNode( const std::string& name ) : + RenderingNode( name, getTypename() ) { + + addInput( m_inObjects ); + m_inObjects->mustBeLinked(); + addInput( m_inCamera ); + m_inCamera->mustBeLinked(); + + // TODO, allow to not provide the two following inputs, then using internal textures ? + addInput( m_inColor ); + m_inColor->mustBeLinked(); + addInput( m_inDepth ); + m_inDepth->mustBeLinked(); + + addInput( m_inActivated ); + + addOutput( m_outColor, m_colorTexture ); + addEditableParameter( m_editableActivate ); +} + +void WireframeRenderingNode::init() { + m_framebuffer = new globjects::Framebuffer(); + + m_nodeState = new globjects::State( globjects::State::DeferredMode ); + m_nodeState->enable( gl::GL_DEPTH_TEST ); + m_nodeState->depthFunc( gl::GL_LESS ); + m_nodeState->depthMask( gl::GL_FALSE ); + m_nodeState->colorMask( gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE ); + m_nodeState->blendFuncSeparate( + gl::GL_SRC_ALPHA, gl::GL_ONE_MINUS_SRC_ALPHA, gl::GL_ONE, gl::GL_ZERO ); + // no glBlendEquationSeparate in globjects::State + m_nodeState->enable( gl::GL_BLEND ); + m_nodeState->polygonOffset( -1.1f, -1.0f ); + m_nodeState->enable( gl::GL_POLYGON_OFFSET_FILL ); +} + +void WireframeRenderingNode::destroy() { + delete m_framebuffer; + + delete m_nodeState; +} + +void WireframeRenderingNode::resize( uint32_t width, uint32_t height ) { + m_wireframeWidth = width; + m_wireframeHeight = height; +} + +void WireframeRenderingNode::buildRenderTechnique( + const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const { + // TODO, use the Radium shader instead of the specific one --> wait for PR "small fixes" (#1035) + // before that + std::string resourcesRootDir = m_resourceDir + "Shaders/WireframeNode/"; + auto mat = const_cast( ro )->getMaterial(); + // Volumes are not used in WireframePass + if ( mat->getMaterialAspect() == Ra::Engine::Data::Material::MaterialAspect::MAT_DENSITY ) { + return; + } + if ( auto cfg = Ra::Engine::Data::ShaderConfigurationFactory::getConfiguration( + { "WireframeNode::WireframeRendering" } ) ) { + rt.setConfiguration( *cfg, m_idx ); + } + else { + Ra::Engine::Data::ShaderConfiguration theConfig { { "WireframeNode::WireframeRendering" }, + resourcesRootDir + "Wireframe.vert.glsl", + resourcesRootDir + + "Wireframe.frag.glsl" }; + theConfig.addShader( Ra::Engine::Data::ShaderType_GEOMETRY, + resourcesRootDir + "Wireframe.geom.glsl" ); + // Add to the ShaderConfigManager + Ra::Engine::Data::ShaderConfigurationFactory::addConfiguration( theConfig ); + // Add to the RenderTechnique + rt.setConfiguration( theConfig, m_idx ); + } + rt.setParametersProvider( mat, m_idx ); +} + +bool WireframeRenderingNode::execute() { + // Get parameters + // Render objects + auto& renderObjects = m_inObjects->getData(); + // Cameras + auto& camera = m_inCamera->getData(); + + // Color tex + m_colorTexture = &m_inColor->getData(); + m_outColor->setData( m_colorTexture ); + + // Depth buffer + auto& depthBuffer = m_inDepth->getData(); + + // activation : + auto activated = m_inActivated->isLinked() ? m_inActivated->getData() : m_activate; + + // Compute the result + if ( activated ) { + if ( m_wireframes.size() > renderObjects.size() ) { m_wireframes.clear(); } + + m_framebuffer->bind(); + m_framebuffer->attachTexture( gl::GL_DEPTH_ATTACHMENT, depthBuffer.texture() ); + m_framebuffer->attachTexture( gl::GL_COLOR_ATTACHMENT0, m_colorTexture->texture() ); + const gl::GLenum buffers[] = { gl::GL_COLOR_ATTACHMENT0 }; + gl::glDrawBuffers( 1, buffers ); + + auto currentState = globjects::State::currentState(); + m_nodeState->apply(); + gl::glBlendEquationSeparate( gl::GL_FUNC_ADD, gl::GL_FUNC_ADD ); + + for ( const auto& ro : renderObjects ) { + if ( ro->isVisible() ) { + std::shared_ptr wro; + + auto it = m_wireframes.find( ro.get() ); + if ( it == m_wireframes.end() ) { + using trimesh = + Ra::Engine::Data::IndexedGeometry; + using polymesh = + Ra::Engine::Data::IndexedGeometry; + // TODO : add here quadmesh as in #925 + std::shared_ptr disp; + auto displayable = ro->getMesh(); + auto tm = std::dynamic_pointer_cast( displayable ); + auto tp = std::dynamic_pointer_cast( displayable ); + if ( tm ) { WireframeNodeInternal::processLineMesh( tm, disp ); } + if ( tp ) { WireframeNodeInternal::processLineMesh( tp, disp ); } + m_wireframes[ro.get()] = disp; + wro = disp; + } + else { wro = it->second; } + + auto shader = m_shaderMngr->getShaderProgram( "WireframeNode::WireframeRendering" ); + if ( wro && shader ) { + wro->updateGL(); + shader->bind(); + Ra::Core::Matrix4 modelMatrix = ro->getTransformAsMatrix(); + shader->setUniform( "transform.proj", camera.projMatrix ); + shader->setUniform( "transform.view", camera.viewMatrix ); + shader->setUniform( "transform.model", modelMatrix ); + shader->setUniform( "viewport", + Ra::Core::Vector2 { m_wireframeWidth, m_wireframeHeight } ); + wro->render( shader ); + } + } + } + + currentState->apply(); + m_framebuffer->unbind(); + } + return true; +} +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderNodes/WireframeRenderingNode.hpp b/src/Dataflow/Rendering/Nodes/RenderNodes/WireframeRenderingNode.hpp new file mode 100644 index 00000000000..4ca29320445 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderNodes/WireframeRenderingNode.hpp @@ -0,0 +1,80 @@ +#pragma once +#include + +#include + +#include +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { + +namespace Nodes { +using namespace Ra::Dataflow::Core; +/** + * \brief Render a scene using an environment map as light source. + * + */ +class RA_DATAFLOW_API WireframeRenderingNode : public RenderingNode +{ + public: + explicit WireframeRenderingNode( const std::string& name ); + + void init() override; + + bool execute() override; + + void destroy() override; + + void resize( uint32_t width, uint32_t height ) override; + + void buildRenderTechnique( const Ra::Engine::Rendering::RenderObject* ro, + Ra::Engine::Rendering::RenderTechnique& rt ) const override; + + bool hasRenderTechnique() override { return true; } + + static const std::string getTypename() { return "Wireframe Node"; } + + protected: + void toJsonInternal( nlohmann::json& data ) const override { data["activated"] = m_activate; } + + bool fromJsonInternal( const nlohmann::json& data ) override { + if ( data.contains( "activated" ) ) { m_activate = data["activated"]; } + else { m_activate = false; } + return true; + } + + private: + Ra::Engine::Data::Texture* m_colorTexture; + + globjects::Framebuffer* m_framebuffer { nullptr }; + + globjects::State* m_nodeState; + + using WireMap = std::map>; + mutable WireMap m_wireframes; + + uint m_wireframeWidth { 0 }; + uint m_wireframeHeight { 0 }; + + bool m_activate { false }; + + EditableParameter* m_editableActivate { + new EditableParameter( "activated", m_activate ) }; + + PortIn* m_inColor { new PortIn( "color", this ) }; + PortIn* m_inDepth { new PortIn( "depth", this ) }; + PortIn* m_inActivated { new PortIn( "activate", this ) }; + PortIn>* m_inObjects { + new PortIn>( "objects", this ) }; + PortIn* m_inCamera { new PortIn( "camera", this ) }; + + PortOut* m_outColor { new PortOut( "Beauty", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderingBuiltInsNodes.cpp b/src/Dataflow/Rendering/Nodes/RenderingBuiltInsNodes.cpp new file mode 100644 index 00000000000..c2145d5f39a --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderingBuiltInsNodes.cpp @@ -0,0 +1,184 @@ +#include +DATAFLOW_LIBRARY_INITIALIZER_DECL( RenderingNodes ); + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { + +namespace Nodes { +// This is here right now, can be moved if requested : +std::string RenderingNode::s_defaultResourceDir; +} // namespace Nodes + +/// Todo, put this somewhere else. This is needed to locate resources by client applications +/// Todo (bis), remove this requirement +static int DataflowRendererMagic = 0x01020304; + +std::string registerRenderingNodesFactories() { + auto resourcesCheck = Ra::Core::Resources::getResourcesPath( + reinterpret_cast( &DataflowRendererMagic ), { "Resources/Radium/Dataflow" } ); + if ( !resourcesCheck ) { + LOG( Ra::Core::Utils::logERROR ) << "Unable to find resources for renderingNodeFactory, " + "setting it to current directory!"; + resourcesCheck = "./"; + } + auto resourcesPath { *resourcesCheck }; + + auto renderingFactory = Core::NodeFactoriesManager::createFactory( "RenderingNodes" ); + + /* --- Sources --- */ + renderingFactory->registerNodeCreator( Nodes::SceneNode::getTypename() + "_", + "Source" ); + renderingFactory->registerNodeCreator( + Nodes::ColorTextureNode::getTypename() + "_", "Source" ); + renderingFactory->registerNodeCreator( + Nodes::DepthTextureNode::getTypename() + "_", "Source" ); + + renderingFactory->registerNodeCreator( + Nodes::EnvMapSourceNode::getTypename() + "_", "Source" ); + + /* --- Sinks --- */ + renderingFactory->registerNodeCreator( + Nodes::DisplaySinkNode::getTypename() + "_", "Sinks" ); + + /* --- operators --- */ + renderingFactory->registerNodeCreator( + Nodes::ClearColorNode::getTypename() + "_", "Render" ); + + renderingFactory->registerNodeCreator( + [resourcesPath, renderingFactory]( const nlohmann::json& data ) { + auto node = new SimpleRenderNode( "SimpleRender_" + + std::to_string( renderingFactory->nextNodeId() ) ); + node->fromJson( data ); + node->setResourcesDir( resourcesPath ); + return node; + }, + "Render" ); + + renderingFactory->registerNodeCreator( + [resourcesPath, renderingFactory]( const nlohmann::json& data ) { + auto node = new GeometryAovsNode( "GeometryAovs_" + + std::to_string( renderingFactory->nextNodeId() ) ); + node->fromJson( data ); + node->setResourcesDir( resourcesPath ); + return node; + }, + "Render" ); + + renderingFactory->registerNodeCreator( + [resourcesPath, renderingFactory]( const nlohmann::json& data ) { + auto node = new EmissivityNode( "Emissivity_" + + std::to_string( renderingFactory->nextNodeId() ) ); + node->fromJson( data ); + node->setResourcesDir( resourcesPath ); + return node; + }, + "Render" ); + + renderingFactory->registerNodeCreator( + [resourcesPath, renderingFactory]( const nlohmann::json& data ) { + auto node = new EnvironmentLightingNode( + "EnLighting_" + std::to_string( renderingFactory->nextNodeId() ) ); + node->fromJson( data ); + node->setResourcesDir( resourcesPath ); + return node; + }, + "Render" ); + + renderingFactory->registerNodeCreator( + [resourcesPath, renderingFactory]( const nlohmann::json& data ) { + auto node = new LocalLightingNode( "LocalLighting_" + + std::to_string( renderingFactory->nextNodeId() ) ); + node->fromJson( data ); + node->setResourcesDir( resourcesPath ); + return node; + }, + "Render" ); + + renderingFactory->registerNodeCreator( + [resourcesPath, renderingFactory]( const nlohmann::json& data ) { + auto node = new VolumeLocalLightingNode( + "VolumeLocalLighting_" + std::to_string( renderingFactory->nextNodeId() ) ); + node->fromJson( data ); + node->setResourcesDir( resourcesPath ); + return node; + }, + "Render" ); + + renderingFactory->registerNodeCreator( + [resourcesPath, renderingFactory]( const nlohmann::json& data ) { + auto node = new TransparentLocalLightingNode( + "TransparentLocalLighting_" + std::to_string( renderingFactory->nextNodeId() ) ); + node->fromJson( data ); + node->setResourcesDir( resourcesPath ); + return node; + }, + "Render" ); + + renderingFactory->registerNodeCreator( + [resourcesPath, renderingFactory]( const nlohmann::json& data ) { + auto node = new WireframeRenderingNode( + "WirframeNode_" + std::to_string( renderingFactory->nextNodeId() ) ); + node->fromJson( data ); + node->setResourcesDir( resourcesPath ); + return node; + }, + "Render" ); + + renderingFactory->registerNodeCreator( + [resourcesPath, renderingFactory]( const nlohmann::json& data ) { + auto node = new SsaoNode( "Ssao_" + std::to_string( renderingFactory->nextNodeId() ) ); + node->fromJson( data ); + node->setResourcesDir( resourcesPath ); + return node; + }, + "Render" ); + + /* --- Image processing --- */ + renderingFactory->registerNodeCreator( + [resourcesPath, renderingFactory]( const nlohmann::json& ) { + auto node = new FxaaNode( "Fxaa_" + std::to_string( renderingFactory->nextNodeId() ) ); + return node; + }, + "Image processing" ); + + /* --- Graphs --- */ + renderingFactory->registerNodeCreator( RenderingGraph::getTypename() + "_", + "Graph" ); + + /* -- end --*/ + Core::NodeFactoriesManager::registerFactory( renderingFactory ); + + return resourcesPath; +} + +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra + +DATAFLOW_LIBRARY_INITIALIZER_IMPL( RenderingNodes ) { + Ra::Dataflow::Rendering::Nodes::RenderingNode::s_defaultResourceDir = + Ra::Dataflow::Rendering::registerRenderingNodesFactories(); +} diff --git a/src/Dataflow/Rendering/Nodes/RenderingBuiltInsNodes.hpp b/src/Dataflow/Rendering/Nodes/RenderingBuiltInsNodes.hpp new file mode 100644 index 00000000000..407e96740b3 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderingBuiltInsNodes.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include +namespace Ra { +namespace Dataflow { +namespace Rendering { + +/** + * \brief Create the node system default factory for rendering nodes. + * + * If needed, the definition of all the rendering nodes can be included using one of the headers + * - #include + * + * \return the default resources location for rendering nodes + */ +RA_DATAFLOW_API std::string registerRenderingNodesFactories(); +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/RenderingNode.hpp b/src/Dataflow/Rendering/Nodes/RenderingNode.hpp new file mode 100644 index 00000000000..dfd8e78f23e --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/RenderingNode.hpp @@ -0,0 +1,102 @@ +#pragma once +#include + +#include + +#include +#include +#include +#include +#include + +namespace Ra::Engine::Data { +class ShaderProgramManager; +} + +void RenderingNodes__Initializer(); + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +/** + * Defining some useful aliases for data type + * + */ + +using RenderObjectPtrType = std::shared_ptr; +using LightPtrType = const Ra::Engine::Scene::Light*; +using CameraType = Ra::Engine::Data::ViewingParameters; +using ColorType = Ra::Core::Utils::Color; +using TextureType = Ra::Engine::Data::Texture; + +/** + * Base class for Rendering nodes. + * Rendering nodes are nodes with some interface needed to render the scene. + */ +class RA_DATAFLOW_API RenderingNode : public Dataflow::Core::Node, + public Ra::Core::Utils::IndexedObject +{ + public: + using Dataflow::Core::Node::Node; + + /// The resize(uint32_t width, uint32_t height) function is called when the application gets + /// resized. Its goal is to resize the potential internal textures if needed. + /// @param width The new width of the surface. + /// @param height The new height of the surface. + virtual void resize( uint32_t width, uint32_t height ) = 0; + + /// Build a render technic per material. + /// @param ro The render object to get the material from + /// @param rt The render technic to build + virtual void buildRenderTechnique( const Ra::Engine::Rendering::RenderObject*, + Ra::Engine::Rendering::RenderTechnique& ) const {}; + + /// Indicate if the node needs to setup a rendertechnique on RenderObjects + virtual bool hasRenderTechnique() { return false; } + + /// Initialize shaders internally used by the node. + /// This method should be override by rendering nodes that requires specific shaders, + /// independent from renderObjects, to compute their result. + virtual bool initInternalShaders() { return true; } + + /// Sets the shader program manager + void setShaderProgramManager( Ra::Engine::Data::ShaderProgramManager* shaderMngr ) { + m_shaderMngr = shaderMngr; + } + + /// Sets the filesystem (real or virtual) location for the node resources (shaders, ...) + void setResourcesDir( std::string resourcesRootDir ) { + m_resourceDir = std::move( resourcesRootDir ); + } + + static const std::string getTypename() { return "RenderingNode"; } + + protected: + void toJsonInternal( nlohmann::json& ) const override { + // Dataflow::Core::Node::toJsonInternal( data ); + } + bool fromJsonInternal( const nlohmann::json& ) override { + /* + auto r = Dataflow::Core::Node::fromJsonInternal( data ); + return r; + */ + return true; + } + + /// The renderer's shader program manager + Ra::Engine::Data::ShaderProgramManager* m_shaderMngr; + /// The resource directory to use + std::string m_resourceDir { s_defaultResourceDir }; + + private: + friend DATAFLOW_LIBRARY_INITIALIZER_DECL( ::RenderingNodes ); + /// The default resources directory for nodes, set once + static std::string s_defaultResourceDir; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/Sinks/DisplaySinkNode.cpp b/src/Dataflow/Rendering/Nodes/Sinks/DisplaySinkNode.cpp new file mode 100644 index 00000000000..b332fd6db5e --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/Sinks/DisplaySinkNode.cpp @@ -0,0 +1,63 @@ +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +DisplaySinkNode::DisplaySinkNode( const std::string& name ) : Node( name, getTypename() ) { + addInput( m_beautyTex ); + m_beautyTex->attachMember( this, &DisplaySinkNode::observeConnection ); + for ( size_t i = 0; i < DisplaySinkNode::MaxImages; i++ ) { + auto portIn = new PortIn( "AOV_" + std::to_string( i ), this ); + addInput( portIn ); + portIn->attachMember( this, &DisplaySinkNode::observeConnection ); + } + m_textures.resize( DisplaySinkNode::MaxImages ); +} +void DisplaySinkNode::init() {} + +DisplaySinkNode::~DisplaySinkNode() { + detachAll(); +} + +bool DisplaySinkNode::execute() { + // check if connections have changed on the input port + bool gotData { false }; + for ( size_t i = 0; i < DisplaySinkNode::MaxImages; i++ ) { + if ( m_inputs[i]->isLinked() ) { + auto input = static_cast*>( m_inputs[i].get() ); + m_textures[i] = &( input->getData() ); + gotData = true; + } + else { m_textures[i] = nullptr; } + } + auto interfacePort = static_cast>*>( m_interface[0] ); + if ( gotData ) { interfacePort->setData( &m_textures ); } + else { interfacePort->setData( nullptr ); } + // not sure DisplaySink should be observable + this->notify( m_textures ); + + return true; +} + +void DisplaySinkNode::observeConnection( + const std::string& /*name*/, + // Will be used for efficient management of connection/de-connection + const PortIn& /* port */, + bool /*connected*/ ) { + // deffer the port management to DisplaySinkNode::execute() +} + +const std::vector& DisplaySinkNode::buildInterfaces( Node* parent ) { + m_interface.clear(); + m_interface.shrink_to_fit(); + m_interface.reserve( 1 ); + auto interfacePort = new PortOut>( "Beauty+AOV", parent ); + m_interface.emplace_back( interfacePort ); + return m_interface; +} +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/Sinks/DisplaySinkNode.hpp b/src/Dataflow/Rendering/Nodes/Sinks/DisplaySinkNode.hpp new file mode 100644 index 00000000000..724f43a8660 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/Sinks/DisplaySinkNode.hpp @@ -0,0 +1,51 @@ +#pragma once +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Dataflow::Core; + +class RA_DATAFLOW_API DisplaySinkNode + : public Node, + public Ra::Core::Utils::Observable&> +{ + public: + static constexpr int MaxImages = 9; + + explicit DisplaySinkNode( const std::string& name ); + ~DisplaySinkNode() override; + + bool execute() override; + void init() override; + + const std::vector& buildInterfaces( Node* parent ) override; + + static const std::string getTypename() { return "Display Sink"; } + + const std::vector& getTextures() const { return m_textures; } + + protected: + void toJsonInternal( nlohmann::json& ) const override {} + bool fromJsonInternal( const nlohmann::json& ) override { return true; } + + private: + std::vector m_textures; + + PortIn* m_beautyTex { new PortIn( "Beauty", this ) }; + + // the observer method + void + observeConnection( const std::string& name, const PortIn& port, bool connected ); +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/Sources/EnvMapSourceNode.hpp b/src/Dataflow/Rendering/Nodes/Sources/EnvMapSourceNode.hpp new file mode 100644 index 00000000000..223cdd433a7 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/Sources/EnvMapSourceNode.hpp @@ -0,0 +1,84 @@ +#pragma once +#pragma once +#include + +#include + +#include +#include + +namespace Ra { +namespace Dataflow { + +namespace Rendering { +using namespace Ra::Engine::Data; +using EnvmapPtrType = std::shared_ptr; + +namespace Nodes { + +using EnvMapSourceNode = Core::Sources::SingleDataSourceNode; + +} +} // namespace Rendering + +namespace Core { +namespace Sources { +template <> +inline const std::string& SingleDataSourceNode::getTypename() { + static std::string typeName { "Source" }; + return typeName; +} + +template <> +inline SingleDataSourceNode::SingleDataSourceNode( + const std::string& name ) : + SingleDataSourceNode( name, SingleDataSourceNode::getTypename() ) { + setEditable( "EnvMap" ); +} + +template <> +inline void +SingleDataSourceNode::toJsonInternal( nlohmann::json& data ) const { + auto envTex = getData(); + if ( envTex->get() ) { + data["files"] = envTex->get()->getImageName().c_str(); + data["strength"] = envTex->get()->getStrength() * 100.; + } +} + +template <> +inline bool +SingleDataSourceNode::fromJsonInternal( const nlohmann::json& data ) { + if ( data.contains( "files" ) ) { + std::string files = data["files"]; + float strength = data["strength"]; + float s = strength / 100.; + // check if the file exists + bool envmap_exist; + auto pos = files.find( ';' ); + if ( pos != std::string::npos ) { + std::string f1 = files.substr( 0, pos - 1 ); + envmap_exist = std::filesystem::exists( f1 ); + } + else { envmap_exist = std::filesystem::exists( files ); } + if ( envmap_exist ) { + auto envmp = std::make_shared( files, true ); + envmp->setStrength( s ); + setData( envmp ); + } + else { + Rendering::EnvmapPtrType nullEnvMap { nullptr }; + setData( nullEnvMap ); + } + } + else { + Rendering::EnvmapPtrType nullEnvMap { nullptr }; + setData( nullEnvMap ); + } + return true; +} + +} // namespace Sources +} // namespace Core +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/Sources/Scene.hpp b/src/Dataflow/Rendering/Nodes/Sources/Scene.hpp new file mode 100644 index 00000000000..124be48693d --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/Sources/Scene.hpp @@ -0,0 +1,79 @@ +#pragma once +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Dataflow::Core; + +class RA_DATAFLOW_API SceneNode : public Node +{ + protected: + SceneNode( const std::string& instanceName, const std::string& typeName ) : + Node( instanceName, typeName ) { + addOutput( m_roOut, m_objects ); + addOutput( m_lightOut, m_lights ); + addOutput( m_cameraOut, m_camera ); + } + + public: + explicit SceneNode( const std::string& name ) : SceneNode( name, SceneNode::getTypename() ) {} + + bool execute() override { + auto interfaceRo = static_cast>*>( m_interface[0] ); + if ( interfaceRo->isLinked() ) { m_roOut->setData( &( interfaceRo->getData() ) ); } + else { m_roOut->setData( m_objects ); } + + auto interfaceLights = static_cast>*>( m_interface[1] ); + if ( interfaceLights->isLinked() ) { + m_lightOut->setData( &( interfaceLights->getData() ) ); + } + else { m_lightOut->setData( m_lights ); } + + auto interfaceCamera = static_cast*>( m_interface[2] ); + if ( interfaceCamera->isLinked() ) { + m_cameraOut->setData( &( interfaceCamera->getData() ) ); + } + else { m_cameraOut->setData( m_camera ); } + return true; + } + + static const std::string getTypename() { return "Scene data provider"; } + + /// Set te scene data collection + /// @note, these collections must have a lifetime longer than the Scene Node they are associated + /// to. + void setScene( std::vector* ros, std::vector* lights ) { + m_objects = ros; + m_lights = lights; + } + + void setCamera( CameraType* camera ) { m_camera = camera; } + + protected: + bool fromJsonInternal( const nlohmann::json& ) override { return true; } + void toJsonInternal( nlohmann::json& ) const override {} + + private: + std::vector* m_objects { nullptr }; + PortOut>* m_roOut { + new PortOut>( "objects", this ) }; + + std::vector* m_lights { nullptr }; + PortOut>* m_lightOut { + new PortOut>( "lights", this ) }; + + CameraType* m_camera { nullptr }; + PortOut* m_cameraOut { new PortOut( "camera", this ) }; +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/Sources/TextureSourceNode.cpp b/src/Dataflow/Rendering/Nodes/Sources/TextureSourceNode.cpp new file mode 100644 index 00000000000..73791188bc0 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/Sources/TextureSourceNode.cpp @@ -0,0 +1,74 @@ +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +TextureSourceNode::TextureSourceNode( const std::string& instanceName, + const Ra::Engine::Data::TextureParameters& texParams ) : + TextureSourceNode( instanceName, getTypename(), texParams ) {} + +TextureSourceNode::TextureSourceNode( const std::string& instanceName, + const std::string& typeName, + const Ra::Engine::Data::TextureParameters& texParams ) : + RenderingNode( instanceName, typeName ) { + if ( !m_texture ) { m_texture = new Ra::Engine::Data::Texture( texParams ); } + addOutput( m_portOut, m_texture ); +} + +bool TextureSourceNode::execute() { + auto interfacePort = static_cast*>( m_interface[0] ); + if ( interfacePort->isLinked() ) { m_texture = &interfacePort->getData(); } + m_portOut->setData( m_texture ); + return true; +} + +void TextureSourceNode::destroy() { + delete m_texture; +} + +void TextureSourceNode::resize( uint32_t width, uint32_t height ) { + m_texture->resize( width, height ); +} + +ColorTextureNode::ColorTextureNode( const std::string& name ) : + TextureSourceNode( name, + getTypename(), + Ra::Engine::Data::TextureParameters { name + " (Color)", + gl::GL_TEXTURE_2D, + 1, + 1, + 1, + gl::GL_RGBA, + gl::GL_RGBA32F, + gl::GL_FLOAT, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_LINEAR, + gl::GL_LINEAR, + nullptr } ) {} + +DepthTextureNode::DepthTextureNode( const std::string& name ) : + TextureSourceNode( name, + getTypename(), + Ra::Engine::Data::TextureParameters { name + " (Depth)", + gl::GL_TEXTURE_2D, + 1, + 1, + 1, + gl::GL_DEPTH_COMPONENT, + gl::GL_DEPTH_COMPONENT24, + gl::GL_UNSIGNED_INT, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_CLAMP_TO_EDGE, + gl::GL_NEAREST, + gl::GL_NEAREST, + nullptr } ) {} + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Nodes/Sources/TextureSourceNode.hpp b/src/Dataflow/Rendering/Nodes/Sources/TextureSourceNode.hpp new file mode 100644 index 00000000000..ff45fe64fa8 --- /dev/null +++ b/src/Dataflow/Rendering/Nodes/Sources/TextureSourceNode.hpp @@ -0,0 +1,56 @@ +#pragma once +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Nodes { + +using namespace Ra::Dataflow::Core; + +class RA_DATAFLOW_API TextureSourceNode : public RenderingNode +{ + public: + TextureSourceNode( const std::string& instanceName, + const Ra::Engine::Data::TextureParameters& texParams ); + + bool execute() override; + void destroy() override; + + void resize( uint32_t width, uint32_t height ) override; + static const std::string getTypename() { return "TextureSource"; } + + protected: + TextureSourceNode( const std::string& instanceName, + const std::string& typeName, + const Ra::Engine::Data::TextureParameters& texParams ); + + private: + // TODO : editable parameter for TextureParameters ? + Ra::Engine::Data::Texture* m_texture { nullptr }; + + PortOut* m_portOut { new PortOut( "texture", this ) }; +}; + +class RA_DATAFLOW_API ColorTextureNode : public TextureSourceNode +{ + public: + explicit ColorTextureNode( const std::string& name ); + static const std::string getTypename() { return "Color Texture"; } +}; + +class RA_DATAFLOW_API DepthTextureNode : public TextureSourceNode +{ + public: + explicit DepthTextureNode( const std::string& name ); + static const std::string getTypename() { return "Depth Texture"; } +}; + +} // namespace Nodes +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Renderer/ControllableRenderer.cpp b/src/Dataflow/Rendering/Renderer/ControllableRenderer.cpp new file mode 100644 index 00000000000..161e6d77723 --- /dev/null +++ b/src/Dataflow/Rendering/Renderer/ControllableRenderer.cpp @@ -0,0 +1,160 @@ +#include + +#include +#include +#include + +#include + +#include + +#include + +using namespace Ra::Engine; +using namespace Ra::Engine::Scene; +using namespace Ra::Engine::Data; +using namespace Ra::Engine::Rendering; +using namespace gl; + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Renderer { +using namespace Ra::Dataflow::Core; + +ControllableRenderer::RendererController::RendererController() : + Ra::Core::Resources::ObservableVoid() {} + +void ControllableRenderer::RendererController::configure( ControllableRenderer* renderer, + int w, + int h ) { + m_attachedRenderer = renderer; + m_shaderMngr = m_attachedRenderer->m_shaderProgramManager; + m_width = w; + m_height = h; +} + +void ControllableRenderer::RendererController::resize( int w, int h ) { + m_width = w; + m_height = h; +} + +// ------------------------------------------------------------------------------------------ +// Interface with Radium renderer ... +// ------------------------------------------------------------------------------------------ + +ControllableRenderer::ControllableRenderer( RendererController& controller ) : + Renderer(), m_controller { controller }, m_name { m_controller.getRendererName() } { + m_controller.attachMember( this, &ControllableRenderer::controllerStateChanged ); +} + +ControllableRenderer::~ControllableRenderer() = default; + +void ControllableRenderer::controllerStateChanged() { + m_controllerStateChanged = true; +} + +bool ControllableRenderer::buildRenderTechnique( Ra::Engine::Rendering::RenderObject* ro ) const { + return m_controller.buildRenderTechnique( ro ); +} + +void ControllableRenderer::initResources() { + // uses several resources from the Radium engine + auto resourcesRootDir { RadiumEngine::getInstance()->getResourcesDir() + "Shaders/" }; + + m_shaderProgramManager->addShaderProgram( + { { "Hdr2Ldr" }, + resourcesRootDir + "2DShaders/Basic2D.vert.glsl", + resourcesRootDir + "2DShaders/Hdr2Ldr.frag.glsl" } ); + + m_postprocessFbo = std::make_unique(); +} + +void ControllableRenderer::initializeInternal() { + auto lmngr = dynamic_cast( + Ra::Engine::RadiumEngine::getInstance()->getSystem( "DefaultLightManager" ) ); + if ( lmngr == nullptr ) { + lmngr = new DefaultLightManager(); + Ra::Engine::RadiumEngine::getInstance()->registerSystem( "DefaultLightManager", lmngr ); + } + m_lightmanagers.push_back( lmngr ); + + // Initialize renderer resources + initResources(); + m_controller.configure( this, m_width, m_height ); + + // TODO update shared textures as the controller modify its output : observe the controller ? + for ( const auto& t : m_sharedTextures ) { + m_secondaryTextures.insert( { t.first, t.second.get() } ); + } +} + +void ControllableRenderer::resizeInternal() { + // Resize the controller + m_controller.resize( m_width, m_height ); + + // Resize the internal resources + m_postprocessFbo->bind(); + m_postprocessFbo->attachTexture( GL_COLOR_ATTACHMENT0, m_fancyTexture->texture() ); + // finished with fbo, unbind to bind default + globjects::Framebuffer::unbind(); +} + +void ControllableRenderer::updateStepInternal( + const Ra::Engine::Data::ViewingParameters& renderData ) { + m_controller.update( renderData ); + if ( m_controllerStateChanged ) { + buildAllRenderTechniques(); + m_controllerStateChanged = false; + } + + // TODO, improve light and camera management to prevent multiple alloc/copy ... + auto lightMngr = getLightManager(); + auto lights = getLights(); + lights->clear(); + lights->reserve( lightMngr->count() ); + for ( size_t i = 0; i < lightMngr->count(); i++ ) { + lights->push_back( lightMngr->getLight( i ) ); + } +} + +void ControllableRenderer::renderInternal( const Ra::Engine::Data::ViewingParameters& renderData ) { + + const auto& renderings = m_controller.render( allRenderObjects(), getLights(), renderData ); + if ( renderings.size() > 0 ) { + m_colorTexture = renderings[0]; // allow to select which image to fetch + } + else { m_colorTexture = nullptr; } +} + +void ControllableRenderer::postProcessInternal( const Ra::Engine::Data::ViewingParameters& ) { + if ( m_colorTexture ) { + m_postprocessFbo->bind(); + + // GL_ASSERT( glDrawBuffers( 1, buffers ) ); + GL_ASSERT( glDrawBuffer( GL_COLOR_ATTACHMENT0 ) ); + GL_ASSERT( glDisable( GL_DEPTH_TEST ) ); + GL_ASSERT( glDepthMask( GL_FALSE ) ); + + auto shader = m_postProcessEnabled + ? m_shaderProgramManager->getShaderProgram( "Hdr2Ldr" ) + : m_shaderProgramManager->getShaderProgram( "DrawScreen" ); + shader->bind(); + shader->setUniform( "screenTexture", m_colorTexture, 0 ); + m_quadMesh->render( shader ); + + GL_ASSERT( glDepthMask( GL_TRUE ) ); + GL_ASSERT( glEnable( GL_DEPTH_TEST ) ); + + m_postprocessFbo->unbind(); + } +} + +void ControllableRenderer::debugInternal( const Ra::Engine::Data::ViewingParameters& ) {} + +void ControllableRenderer::uiInternal( const Ra::Engine::Data::ViewingParameters& ) {} + +} // namespace Renderer +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Renderer/ControllableRenderer.hpp b/src/Dataflow/Rendering/Renderer/ControllableRenderer.hpp new file mode 100644 index 00000000000..f2ede8222fa --- /dev/null +++ b/src/Dataflow/Rendering/Renderer/ControllableRenderer.hpp @@ -0,0 +1,174 @@ +#pragma once +#include + +/** + * Right now a renderer own a graph with some specific nodes available that gives acces to the + * renderobjects, the camera and the lights. + * These nodes are put in a specific factory SceneAccessors which contains nodes to access the + * renderobject, the camera and the light of the scene. + * + */ + +#include +#include + +#include + +namespace globjects { +class Framebuffer; +} + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Renderer { + +/** Dataflow renderer for the Radium Engine + * This Renderer is fully configurable, either dynamically or programmatically. + * It implements the Ra::Engine::Rendering/Renderer interface. + * + * A NodeBasedRenderer is configured by using the RenderControlFunctor given at construction. + * A RenderControlFunctor offers the following services : + * - configure() : add to the renderer as many RadiumNBR::RenderPass as needed. This method is + * called once when initializing the renderer. This method could also initialize internal + * resources into the controller that could be used to control the rendering. + * - resize() : called each time the renderer output is resized. This will allow modify controller + * resources that depends on the size of the output (e.g. internal textures ...) + * - update() : Called once before each frame to update the internal state of the renderer. + * + * A NodeBasedRenderer defines two textures that might be shared between passes : + * - a depth buffer attachable texture, stored with the key "Depth (RadiumNBR)" into the shared + * textures collection + * - a Linear space RGBA color texture, stored with the key "Linear RGB (RadiumNBR)" into the + * shared textures collection + * + * If requested on the base Ra::Engine::Rendering::Renderer, a NodeBasedRenderer apply a + * post-process step on the "Linear RGB (RadiumNBR)" that convert colors from linearRGB to sRGB + * color space before displaying the image. + * + * + * @see rendering.md for description of the renderer + */ +class RA_DATAFLOW_API ControllableRenderer : public Ra::Engine::Rendering::Renderer +{ + + public: + /** + * Renderer controller + */ + class RA_DATAFLOW_API RendererController : public Ra::Core::Resources::ObservableVoid + { + + public: + RendererController(); + virtual ~RendererController() = default; + RendererController( const RendererController& ) = delete; + RendererController( const RendererController&& ) = delete; + RendererController& operator=( RendererController&& ) = delete; + RendererController& operator=( const RendererController& ) = delete; + + /// Configuration function. + /// Called once at the configuration of the renderer + virtual void configure( ControllableRenderer* renderer, int w, int h ); + + /// Resize function + /// Called each time the renderer is resized + virtual void resize( int w, int h ); + + /// Update function + /// Called once before each frame to update the internal state of the renderer + virtual void update( const Ra::Engine::Data::ViewingParameters& renderData ) = 0; + + /// RenderTechnique builder + /// Called each time the render techniques should be built (aftre switching from renderer, + /// loading a scene, ...). + virtual bool buildRenderTechnique( Ra::Engine::Rendering::RenderObject* ro ) const = 0; + + /// Render the given scene + /// \return true if rendering output is available + virtual const std::vector& render( std::vector* ros, + std::vector* lights, + const CameraType& cameras ) const = 0; + + [[nodiscard]] virtual std::string getRendererName() const { + return "Base RendererController"; + } + + protected: + ControllableRenderer* m_attachedRenderer; + Ra::Engine::Data::ShaderProgramManager* m_shaderMngr; + int m_width { -1 }; + int m_height { -1 }; + }; + + /// Construct a renderer configured and managed through the controller + explicit ControllableRenderer( RendererController& controller ); + + /// The destructor is used to destroy the render graph + ~ControllableRenderer() override; + + [[nodiscard]] std::string getRendererName() const override { return m_name; } + + bool buildRenderTechnique( Ra::Engine::Rendering::RenderObject* ro ) const override; + + /// Access the default light manager + Ra::Engine::Scene::LightManager* getLightManager() { return m_lightmanagers[0]; } + + /// Access the controller + RendererController& getController() { return m_controller; } + + protected: + void initializeInternal() override; + void resizeInternal() override; + void updateStepInternal( const Ra::Engine::Data::ViewingParameters& renderData ) override; + void renderInternal( const Ra::Engine::Data::ViewingParameters& renderData ) override; + void postProcessInternal( const Ra::Engine::Data::ViewingParameters& renderData ) override; + void debugInternal( const Ra::Engine::Data::ViewingParameters& renderData ) override; + void uiInternal( const Ra::Engine::Data::ViewingParameters& renderData ) override; + + /** Initialize internal resources for the renderer. + * The base function creates the depth and final color texture to be shared by the rendering + * passes that need them. + */ + virtual void initResources(); + + public: + inline std::map>& sharedTextures() { + return m_sharedTextures; + } + + inline globjects::Framebuffer* postprocessFbo() { return m_postprocessFbo.get(); } + + inline std::vector* allRenderObjects() { return &m_fancyRenderObjects; } + + inline std::vector* getLights() { return &m_lights; } + + private: + /// Controller observer method + void controllerStateChanged(); + + bool m_controllerStateChanged { false }; + + /// textures own by the Renderer but shared across passes + std::map> m_sharedTextures; + + /// internal FBO used for post-processing + std::unique_ptr m_postprocessFbo; + + /// The configurator functor to use + RendererController& m_controller; + + /// The name of the renderer + std::string m_name { "RenderGraph renderer" }; + + /// Texture to be read for postprocess according to the display node + Ra::Engine::Data::Texture* m_colorTexture { nullptr }; + + /// Vector of lights ... + std::vector m_lights; +}; + +} // namespace Renderer +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Renderer/RenderGraphController.cpp b/src/Dataflow/Rendering/Renderer/RenderGraphController.cpp new file mode 100644 index 00000000000..6bfc161d967 --- /dev/null +++ b/src/Dataflow/Rendering/Renderer/RenderGraphController.cpp @@ -0,0 +1,177 @@ +#include + +#include +#include +#include + +#include + +#include + +#include + +using namespace Ra::Engine; +using namespace Ra::Engine::Scene; +using namespace Ra::Engine::Data; +using namespace Ra::Engine::Rendering; +using namespace gl; + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Renderer { +using namespace Ra::Dataflow::Core; + +RenderGraphController::RenderGraphController() : ControllableRenderer::RendererController() {} + +void RenderGraphController::configure( ControllableRenderer* renderer, int w, int h ) { + ControllableRenderer::RendererController::configure( renderer, w, h ); + if ( !m_graphToLoad.empty() ) { + loadGraph( m_graphToLoad ); + m_graphToLoad = ""; + } +} + +void RenderGraphController::resize( int w, int h ) { + ControllableRenderer::RendererController::resize( w, h ); + if ( m_renderGraph ) { m_renderGraph->resize( m_width, m_height ); } +} + +void RenderGraphController::compile( bool notifyObservers ) const { + if ( !m_renderGraph->isCompiled() ) { + // compile the model + m_renderGraph->compile(); + // notify the view the model changes + if ( notifyObservers ) { notify(); } + // notify the model the view may have changed + m_renderGraph->resize( m_width, m_height ); + // fetch the data setters and getters from the graph + m_renderGraphInputs = m_renderGraph->getAllDataSetters(); + m_renderGraphOutputs = m_renderGraph->getAllDataGetters(); + } +} +void RenderGraphController::update( const Ra::Engine::Data::ViewingParameters& ) { + if ( m_renderGraph ) { + // Compile and notify the observers in case of state change. + compile( true ); + } +} + +bool RenderGraphController::buildRenderTechnique( Ra::Engine::Rendering::RenderObject* ro ) const { + if ( m_renderGraph ) { + // Only the first call of compile will effectively compile the graph + // do not notify observers here + compile(); + m_renderGraph->buildRenderTechnique( ro ); + return true; + } + return false; +} + +const std::vector& +RenderGraphController::render( std::vector* ros, + std::vector* lights, + const CameraType& cameras ) const { + m_images.clear(); + m_images.shrink_to_fit(); + + // remove the const using const_cast as scene node expect non const object + auto& localCamera = const_cast( cameras ); + + bool status = false; + + if ( m_renderGraph && m_renderGraph->isCompiled() ) { + // set input data + for ( const auto& [ptr, name, type] : m_renderGraphInputs ) { + if ( type == simplifiedDemangledType( *ros ) ) { + ptr->setData( ros ); + m_renderGraph->activateDataSetter( name ); + } + if ( type == simplifiedDemangledType( *lights ) ) { + ptr->setData( lights ); + m_renderGraph->activateDataSetter( name ); + } + if ( type == simplifiedDemangledType( localCamera ) ) { + ptr->setData( &localCamera ); + m_renderGraph->activateDataSetter( name ); + } + } + + // execute the graph + status = m_renderGraph->execute(); + + if ( status ) { + // reset the status so that it will indicate that images are available + status = false; + // get output + // expect it is sufficient + m_images.reserve( m_renderGraphOutputs.size() * 9 ); + for ( const auto& [ptr, name, type] : m_renderGraphOutputs ) { + if ( ptr->hasData() ) { + status = true; + if ( ptr->getTypeName() == simplifiedDemangledType() ) { + // Try a simple texture + auto tex = ptr->getData(); + if ( tex != nullptr ) { m_images.push_back( tex ); } + continue; + } + if ( ptr->getTypeName() == + simplifiedDemangledType>() ) { + // Try a texture vector + const auto& texv = ptr->getData>(); + for ( auto t : texv ) { + if ( t != nullptr ) { m_images.push_back( t ); } + } + continue; + } + LOG( Ra::Core::Utils::logWARNING ) + << "Fetching from " << ptr->getName() << " (" << ptr->getTypeName() + << ") : type not supported !"; + } + } + } + } + + if ( !status ) { + LOG( Ra::Core::Utils::logWARNING ) << " Graph execution failed : no images generated!"; + } + + return m_images; +} + +void RenderGraphController::loadGraph( const std::string& filename ) { + auto loadedGraph = + dynamic_cast( DataflowGraph::loadGraphFromJsonFile( filename ) ); + if ( loadedGraph ) { + m_renderGraph.reset( loadedGraph ); + m_renderGraph->setShaderProgramManager( m_shaderMngr ); + notify(); + } + else { + LOG( Ra::Core::Utils::logERROR ) + << "RenderGraphController::loadGraph : unable to load " << filename; + } +} + +void RenderGraphController::deferredLoadGraph( const std::string& filename ) { + m_graphToLoad = filename; +} + +void RenderGraphController::saveGraph( const std::string& filename ) { + if ( m_renderGraph ) { + auto graphName = filename.substr( filename.find_last_of( '/' ) + 1 ); + m_renderGraph->saveToJson( filename ); + m_renderGraph->setInstanceName( graphName ); + } +} + +void RenderGraphController::resetGraph() { + m_renderGraph.release(); + m_renderGraph = std::make_unique( "untitled" ); + m_renderGraph->setShaderProgramManager( m_shaderMngr ); +} + +} // namespace Renderer +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Renderer/RenderGraphController.hpp b/src/Dataflow/Rendering/Renderer/RenderGraphController.hpp new file mode 100644 index 00000000000..90a8ec2c1d2 --- /dev/null +++ b/src/Dataflow/Rendering/Renderer/RenderGraphController.hpp @@ -0,0 +1,73 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { +namespace Renderer { + +/** + * RenderGraph controller + */ +class RA_DATAFLOW_API RenderGraphController : public ControllableRenderer::RendererController +{ + + public: + RenderGraphController(); + virtual ~RenderGraphController() = default; + RenderGraphController( const RenderGraphController& ) = delete; + RenderGraphController( const RenderGraphController&& ) = delete; + RenderGraphController& operator=( RenderGraphController&& ) = delete; + RenderGraphController& operator=( const RenderGraphController& ) = delete; + + /// Configuration function. + /// Called once at the configuration of the renderer + void configure( ControllableRenderer* renderer, int w, int h ) override; + + /// Resize function + /// Called each time the renderer is resized + void resize( int w, int h ) override; + + /// Update function + /// Called once before each frame to update the internal state of the renderer + void update( const Ra::Engine::Data::ViewingParameters& renderData ) override; + + /// RenderTechnique builder + /// Called each time the render techniques should be built. + bool buildRenderTechnique( Ra::Engine::Rendering::RenderObject* ro ) const override; + + const std::vector& render( std::vector* ros, + std::vector* lights, + const CameraType& cameras ) const override; + + [[nodiscard]] std::string getRendererName() const override { return "Node Renderer"; } + + void loadGraph( const std::string& filename ); + void saveGraph( const std::string& filename ); + void resetGraph(); + /// Call this to set a graph to load before OpenGL is OK + void deferredLoadGraph( const std::string& filename ); + + // allow to "edit" the graph + RenderingGraph* getGraph() { return m_renderGraph.get(); } + + protected: + /// The controlled graph. + /// The controller own the graph and manage loading/saving of the renderer + std::unique_ptr m_renderGraph { nullptr }; + + mutable std::vector m_renderGraphInputs; + mutable std::vector m_renderGraphOutputs; + mutable std::vector m_images; + + std::string m_graphToLoad; + + void compile( bool notifyObservers = false ) const; +}; + +} // namespace Renderer +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/RenderingGraph.cpp b/src/Dataflow/Rendering/RenderingGraph.cpp new file mode 100644 index 00000000000..63194bec766 --- /dev/null +++ b/src/Dataflow/Rendering/RenderingGraph.cpp @@ -0,0 +1,76 @@ +#include + +#include +#include + +using namespace Ra::Engine::Rendering; + +namespace Ra { +namespace Dataflow { +namespace Rendering { +using namespace Ra::Dataflow::Rendering::Nodes; +using namespace Ra::Dataflow::Core; + +void RenderingGraph::init() { + DataflowGraph::init(); +} + +std::pair RenderingGraph::addNode( std::unique_ptr newNode ) { + auto [added, node] = DataflowGraph::addNode( std::move( newNode ) ); + if ( added ) { + // Todo : is there something to do ? + } + return { added, node }; +} + +bool RenderingGraph::removeNode( Node*& node ) { + auto removed = DataflowGraph::removeNode( node ); + if ( removed ) { + // Todo : is there something to do ? (node is already deleted by DataflowGraph::removeNode) + } + return removed; +} + +bool RenderingGraph::compile() { + m_renderingNodes.clear(); + m_rtIndexedNodes.clear(); + auto compiled = DataflowGraph::compile(); + if ( compiled ) { + const auto& compiledNodes = getNodesByLevel(); + int idx = 1; // The renderTechnique id = 0 is reserved for ui/debug objects + for ( const auto& lvl : compiledNodes ) { + for ( auto n : lvl ) { + auto renderNode = dynamic_cast( n ); + if ( renderNode != nullptr ) { + m_renderingNodes.push_back( renderNode ); + if ( renderNode->hasRenderTechnique() ) { + renderNode->setIndex( idx++ ); + m_rtIndexedNodes.push_back( renderNode ); + } + renderNode->setShaderProgramManager( m_shaderMngr ); + renderNode->initInternalShaders(); + } + } + } + } + return compiled; +} + +void RenderingGraph::clearNodes() { + DataflowGraph::clearNodes(); + m_renderingNodes.clear(); + m_rtIndexedNodes.clear(); +} + +void RenderingGraph::buildRenderTechnique( Ra::Engine::Rendering::RenderObject* ro ) const { + auto rt = std::make_shared(); + for ( const auto& rn : m_rtIndexedNodes ) { + rn->buildRenderTechnique( ro, *rt ); + } + rt->updateGL(); + ro->setRenderTechnique( rt ); +} + +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/RenderingGraph.hpp b/src/Dataflow/Rendering/RenderingGraph.hpp new file mode 100644 index 00000000000..13fad0f6f54 --- /dev/null +++ b/src/Dataflow/Rendering/RenderingGraph.hpp @@ -0,0 +1,89 @@ +#pragma once +#include + +#include + +#include + +namespace Ra { +namespace Dataflow { +namespace Rendering { + +using namespace Ra::Dataflow::Rendering::Nodes; +using namespace Ra::Dataflow::Core; + +class RA_DATAFLOW_API RenderingGraph : public DataflowGraph +{ + public: + explicit RenderingGraph( const std::string& name ); + ~RenderingGraph() override = default; + + // Remove the 3 following methods if there is no need to specialize + void init() override; + std::pair addNode( std::unique_ptr newNode ) override; + bool removeNode( Node*& node ) override; + + // These methods are specialized to identify rendering nodes (and those who needs + // rendertechnique) + void clearNodes() override; + bool compile() override; + + /// Sets the shader program manager + void setShaderProgramManager( Ra::Engine::Data::ShaderProgramManager* shaderMngr ) { + m_shaderMngr = shaderMngr; + } + + /// Resize all the rendering output + void resize( uint32_t width, uint32_t height ); + + /// Set render techniques needed by the rendering nodes + void buildRenderTechnique( Ra::Engine::Rendering::RenderObject* ro ) const; + /// Return the typename of the Graph + static const std::string& getTypename(); + + protected: + bool fromJsonInternal( const nlohmann::json& data ) override; + void toJsonInternal( nlohmann::json& ) const override; + + private: + /// The renderer's shader program manager + Ra::Engine::Data::ShaderProgramManager* m_shaderMngr { nullptr }; + /// List of nodes that requires some particular processing + std::vector m_renderingNodes; // to resize + std::vector m_rtIndexedNodes; // associate an index and buildRenderTechnique +}; + +// ----------------------------------------------------------------- +// ---------------------- inline methods --------------------------- + +inline RenderingGraph::RenderingGraph( const std::string& name ) : + DataflowGraph( name, getTypename() ) { + // A rendering graph always use the builtin RenderingNodes factory + addFactory( NodeFactoriesManager::getFactory( "RenderingNodes" ) ); +} + +inline const std::string& RenderingGraph::getTypename() { + static std::string demangledTypeName { "Rendering Graph" }; + return demangledTypeName; +} + +inline void RenderingGraph::resize( uint32_t width, uint32_t height ) { + for ( auto rn : m_renderingNodes ) { + rn->resize( width, height ); + } +} + +inline bool RenderingGraph::fromJsonInternal( const nlohmann::json& data ) { + auto r = DataflowGraph::fromJsonInternal( data ); + // todo, extract RenderingGraph specific data + return r; +} + +inline void RenderingGraph::toJsonInternal( nlohmann::json& data ) const { + DataflowGraph::toJsonInternal( data ); + // todo, add RenderingGraph specific data +} + +} // namespace Rendering +} // namespace Dataflow +} // namespace Ra diff --git a/src/Dataflow/Rendering/Shaders/EmissivityNode/shader.frag.glsl b/src/Dataflow/Rendering/Shaders/EmissivityNode/shader.frag.glsl new file mode 100644 index 00000000000..79b7e213a6e --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/EmissivityNode/shader.frag.glsl @@ -0,0 +1,17 @@ +#include "VertexAttribInterface.frag.glsl" +//------------------- main --------------------- +uniform sampler2D amb_occ_sampler; + +layout (location = 0) out vec4 out_color; +void main() { + // discard non opaque fragment + vec4 bc = getBaseColor(material, getPerVertexTexCoord()); + if (toDiscard(material, bc)) { + discard; + } + + vec2 size = textureSize(amb_occ_sampler, 0).xy; + vec3 ao = texture(amb_occ_sampler, gl_FragCoord.xy/size).rgb; + + out_color = vec4(bc.rgb * 0.01 * ao + getEmissiveColor(material, getPerVertexTexCoord()), 1.0); +} diff --git a/src/Dataflow/Rendering/Shaders/EnvLightNode/shader.frag.glsl b/src/Dataflow/Rendering/Shaders/EnvLightNode/shader.frag.glsl new file mode 100644 index 00000000000..f56cbf9049d --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/EnvLightNode/shader.frag.glsl @@ -0,0 +1,125 @@ +/* +the functions toDiscard and getNormals are given by the implementation of the Material Interface. +The appropriate code will be added at runtime by the shader composer or the render pass initialiser. +*/ + +// Include the VertexAttribInterface so that all materials might be OK. +#include "VertexAttribInterface.frag.glsl" +const float OneOverPi = 0.3183098862; + +layout( location = 5 ) in vec3 in_viewVector; + +uniform sampler2D amb_occ_sampler; + +uniform samplerCube envTexture; +uniform int numLod; + +uniform mat4 redShCoeffs; +uniform mat4 greenShCoeffs; +uniform mat4 blueShCoeffs; + +layout( location = 0 ) out vec4 out_color; + +// For debug, to be removed once finalized. +uniform float envStrength; + +//------------------- main --------------------- +// TODO : verify the computations as there is huge differences between this rendering and GLTF +// see +// https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/source/Renderer/shaders/ibl.glsl + +// sample implementation +void main() { + vec3 tc = getPerVertexTexCoord(); + // discard non opaque fragment + vec4 bc = getBaseColor( material, tc ); + if ( toDiscard( material, bc ) ) discard; + +#ifdef GLTF_MATERIAL_INTERFACE + // Experiment on a new GLSL/Material interface allowing more efficient PBR and composition + + // Compute the normal closure. Do normal map and capture state about normal and tangent space + NormalInfo nrm_info = getNormalInfo( material.baseMaterial, tc ); + vec3 view = normalize( in_viewVector ); + + MaterialInfo bsdf_params; + BsdfInfo layers; + + if ( getSeparateBSDFComponent( material, + tc, + view, + nrm_info, + bsdf_params, + layers ) == 1 ) { + + vec2 size = textureSize( amb_occ_sampler, 0 ).xy; + vec3 ao = texture( amb_occ_sampler, gl_FragCoord.xy / size ).rgb; + + float r = GGXroughness( bsdf_params ); + // diffuse irradiance map + vec4 nrm = vec4( nrm_info.n, 1 ); + vec3 irradiance = vec3( + dot( nrm, redShCoeffs * nrm ), + dot( nrm, greenShCoeffs * nrm ), + dot( nrm, blueShCoeffs * nrm ) + ); + layers.f_diffuse *= irradiance; + + // Specular envmap + vec3 rfl = reflect( -view, nrm_info.n ); + layers.f_specular *= textureLod( envTexture, rfl, r * numLod ).rgb ; + +#ifdef CLEARCOAT_LAYER + // clearcoat envmap + r = pow(bsdf_params.clearcoat.intrough.y, 0.5); + layers.f_clearcoat *= textureLod( envTexture, rfl, r * numLod ).rgb ; +#endif + +#ifdef SHEEN_LAYER + r = pow(bsdf_params.sheen.sheenColorRough.a, 0.5); + layers.f_sheen *= textureLod( envTexture, rfl, r * numLod ).rgb ; +#endif + + bc.rgb = combineLayers( bsdf_params, layers, nrm_info, view ); + + bc.rgb *= ao * envStrength; + } else { + bc.rgb = vec3(0); + } + +#else + vec4 normalWorld = vec4( + getNormal( + material, tc, getWorldSpaceNormal(), getWorldSpaceTangent(), getWorldSpaceBiTangent() ), + 1 ); + + vec3 diffuse; + vec3 specular; + vec3 view = normalize( in_viewVector ); + vec3 rfl = reflect( -view, normalWorld.xyz ); + + if ( getSeparateBSDFComponent( + material, getPerVertexTexCoord(), rfl, view, normalWorld.xyz, diffuse, specular ) == + 1 ) + { + vec2 size = textureSize( amb_occ_sampler, 0 ).xy; + vec3 ao = texture( amb_occ_sampler, gl_FragCoord.xy / size ).rgb; + bc.rgb = diffuse; + vec3 irradiance = vec3( + dot( normalWorld, redShCoeffs * normalWorld ), + dot( normalWorld, greenShCoeffs * normalWorld ), + dot( normalWorld, blueShCoeffs * normalWorld ) + ); + bc.rgb *= irradiance; + // Specular envmap + float cosTi = clamp( dot( rfl, normalWorld.xyz ), 0.001, 1. ); + vec3 spec = clamp( specular * cosTi * OneOverPi * 0.5, 0.001, 1. ); + float r = getGGXRoughness( material, getPerVertexTexCoord() ) * numLod; + bc.rgb += textureLod( envTexture, rfl, r ).rgb * spec; + bc.rgb *= ao * envStrength; + } + else + { bc.rgb = vec3( 0 ); } +#endif + out_color = vec4( bc.rgb, 1 ); +} diff --git a/src/Dataflow/Rendering/Shaders/GeometryAovs/shader.frag.glsl b/src/Dataflow/Rendering/Shaders/GeometryAovs/shader.frag.glsl new file mode 100644 index 00000000000..653fdc2e701 --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/GeometryAovs/shader.frag.glsl @@ -0,0 +1,29 @@ +/* +the functions used to discard or shade the fragment are given by the implementation of the Material +Interface. The appropriate code will be added at runtime by the shader composer or the render pass +initialiser. +*/ +#include "VertexAttribInterface.frag.glsl" + +layout( location = 0 ) out vec4 out_worldpos; +layout( location = 1 ) out vec4 out_normal; + +void main() { + vec3 tc = getPerVertexTexCoord(); + // discard non opaque fragment + vec4 bc = getBaseColor( material, tc ); + if ( toDiscard( material, bc ) ) discard; + +#ifdef GLTF_MATERIAL_INTERFACE + // Experiment on a new GLSL/Material interface allowing more efficient PBR and composition + + // Compute the normal closure. Do normal map and capture state about normal and tangent space + NormalInfo nrm_info = getNormalInfo( material.baseMaterial, tc ); + vec3 worldNormal = nrm_info.n; +#else + vec3 worldNormal = getNormal( + material, tc, getWorldSpaceNormal(), getWorldSpaceTangent(), getWorldSpaceBiTangent() ); +#endif + out_worldpos = getWorldSpacePosition(); + out_normal = vec4( worldNormal * 0.5 + 0.5, 1.0 ); +} diff --git a/src/Dataflow/Rendering/Shaders/LocalLightNode/shader.frag.glsl b/src/Dataflow/Rendering/Shaders/LocalLightNode/shader.frag.glsl new file mode 100644 index 00000000000..cd1d4df8589 --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/LocalLightNode/shader.frag.glsl @@ -0,0 +1,87 @@ +/* +the functions used to discard or shade the fragment are given by the implementation of the Material +Interface. The appropriate code will be added at runtime by the shader composer or the render pass +initialiser. +*/ +#include "DefaultLight.glsl" +#include "VertexAttribInterface.frag.glsl" + +layout( location = 5 ) in vec3 in_viewVector; +layout( location = 6 ) in vec3 in_lightVector; + +uniform sampler2D amb_occ_sampler; + +layout( location = 0 ) out vec4 out_color; // Position in World Space + +//------------------- main --------------------- + +void main() { + /// quick access to texture coordinates + vec3 tc = getPerVertexTexCoord(); + // discard non opaque fragment + vec4 bc = getBaseColor( material, tc ); + if ( toDiscard( material, bc ) ) discard; + +#ifdef GLTF_MATERIAL_INTERFACE + // Experiment on a new GLSL/Material interface allowing more efficient PBR and composition + + // Compute the normal closure. Do normal map and capture state about normal and tangent space + NormalInfo nrm_info = getNormalInfo( material.baseMaterial, tc ); + + // Compute the BSDF closure. Compte all view and lighting independant propserties on the + // material + MaterialInfo bsdf_params; + extractBSDFParameters( material, tc, nrm_info, bsdf_params ); + + // get dynamic lighting informations : wiew and light properties + vec3 wo = normalize( in_viewVector ); // outgoing direction + + // Per local light computation + vec3 wi = normalize( in_lightVector ); // incident direction + vec3 lighting = lightContributionFrom( light, getWorldSpacePosition().xyz ); + + // evaluate the BSDF : get a set of layers representing several optical effects + BsdfInfo layers = evaluateBSDF( material, + bsdf_params, + nrm_info, + wi, + wo, + lighting ); + + // Final evaluation of BSDF/layers closure + vec3 color = combineLayers( bsdf_params, layers, nrm_info, wo ); + +#else + // All vectors are in world space + // A material is always evaluated in the fragment local Frame + // compute matrix from World to local Frame + vec3 normalWorld = getWorldSpaceNormal(); // normalized interpolated normal + vec3 tangentWorld = getWorldSpaceTangent(); // normalized tangent + vec3 binormalWorld = getWorldSpaceBiTangent(); // normalized bitangent + + // Apply normal mapping + normalWorld = getNormal( material, + tc, + normalWorld, + tangentWorld, + binormalWorld ); // normalized bump-mapped normal + binormalWorld = normalize( cross( normalWorld, tangentWorld ) ); // normalized tangent + tangentWorld = normalize( cross( binormalWorld, normalWorld ) ); // normalized bitangent + + mat3 world2local; + world2local[0] = vec3( tangentWorld.x, binormalWorld.x, normalWorld.x ); + world2local[1] = vec3( tangentWorld.y, binormalWorld.y, normalWorld.y ); + world2local[2] = vec3( tangentWorld.z, binormalWorld.z, normalWorld.z ); + // transform all vectors in local frame so that N = (0, 0, 1); + vec3 lightDir = world2local * normalize( in_lightVector ); // incident direction + vec3 viewDir = world2local * normalize( in_viewVector ); // outgoing direction + + vec3 color = evaluateBSDF( material, tc, lightDir, viewDir ) * + lightContributionFrom( light, getWorldSpacePosition().xyz ); +#endif + + vec2 size = textureSize( amb_occ_sampler, 0 ).xy; + vec3 ao = texture( amb_occ_sampler, gl_FragCoord.xy / size ).rgb; + + out_color = vec4( color * ao, 1.0 ); +} diff --git a/src/Dataflow/Rendering/Shaders/SsaoNode/blurao.frag.glsl b/src/Dataflow/Rendering/Shaders/SsaoNode/blurao.frag.glsl new file mode 100644 index 00000000000..cd317e59b9e --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/SsaoNode/blurao.frag.glsl @@ -0,0 +1,20 @@ +layout (location = 0) out vec4 out_ssao; + +uniform sampler2D ao_sampler; +in vec2 varTexcoord; + +const int half_width = 2; + +void main() { + vec2 texelSize = 1.0 / vec2(textureSize(ao_sampler, 0)); + float result = 0.0; + for (int x = -half_width; x < half_width; ++x) + { + for (int y = -half_width; y < half_width; ++y) + { + vec2 offset = vec2(float(x), float(y)) * texelSize; + result += texture(ao_sampler, varTexcoord + offset).r; + } + } + out_ssao = vec4(vec3(result / (4 * half_width * half_width)), 1); +} diff --git a/src/Dataflow/Rendering/Shaders/SsaoNode/ssao.frag.glsl b/src/Dataflow/Rendering/Shaders/SsaoNode/ssao.frag.glsl new file mode 100644 index 00000000000..48eeb582e78 --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/SsaoNode/ssao.frag.glsl @@ -0,0 +1,57 @@ +#include "TransformStructs.glsl" + +layout (location = 0) out vec4 out_ssao; + +uniform sampler2D normal_sampler; +uniform sampler2D position_sampler; +uniform sampler2DRect dir_sampler; + +uniform Transform transform; +uniform float ssdoRadius; + +// TODO compute a bias "a la" glPolygonOffset so that it adapts to z dynamic range. +const float bias = 0.001; + +void main() { + // Convert screen space position to world space position + vec2 size = vec2(textureSize(position_sampler, 0)); + + // The point to shade + vec4 ptInWorld = texelFetch(position_sampler, ivec2(gl_FragCoord.xy), 0); + float ssdo = 0; + if (ptInWorld.w != 0) { + int nbSamples = textureSize(dir_sampler).x; + // The point normal (0 if the fragment must be discarded) + vec3 nrmInWorld = texelFetch(normal_sampler, ivec2(gl_FragCoord.xy), 0).xyz; + float fragDepth = (transform.view * vec4(ptInWorld.xyz, 1)).z; + nrmInWorld = nrmInWorld * 2. -1.; + + for (int i=0;i 0) { + offset *= offset.w * ssdoRadius; + // theSample is the world position, displaced and reprojected. + // It only serve to access the fragment for whihc the depth must be compared. + vec4 theSample = vec4(ptInWorld.xyz + offset.xyz, 1); + + theSample = transform.mvp * theSample; + theSample.xyz /= theSample.w; + theSample.xyz = theSample.xyz * 0.5 + 0.5; + + vec4 sampledPoint = texture(position_sampler, theSample.st); + if (sampledPoint.w != 0) { + float sampleDepth = (transform.view * sampledPoint).z; + + float rangeCheck = smoothstep(0.0, 1.0, ssdoRadius / abs(fragDepth - sampleDepth)); + ssdo += (sampleDepth >= fragDepth + bias ? 1.0 : 0.0) * rangeCheck; + } + } + } + ssdo = 1 - ssdo/nbSamples; + //ssdo *= ssdo; + ssdo = smoothstep(0, 1, ssdo); + } + out_ssao = vec4(vec3(ssdo), 1); +} diff --git a/src/Dataflow/Rendering/Shaders/TransparencyLocalLightNode/shader.frag.glsl b/src/Dataflow/Rendering/Shaders/TransparencyLocalLightNode/shader.frag.glsl new file mode 100644 index 00000000000..137f5267093 --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/TransparencyLocalLightNode/shader.frag.glsl @@ -0,0 +1,108 @@ +/* +the functions toDiscard and getNormals are given by the implementation of the Material Interface. +The appropriate code will be added at runtime by the shader composer or the render pass initialiser. +*/ +#include "DefaultLight.glsl" +#include "VertexAttribInterface.frag.glsl" + +layout( location = 0 ) out vec4 f_Accumulation; +layout( location = 1 ) out vec4 f_Revealage; + +layout( location = 5 ) in vec3 in_viewVector; +layout( location = 6 ) in vec3 in_lightVector; + +// uniform sampler2D amb_occ_sampler; + +// implementation of weight functions of the paper +// Weighted Blended Order-Independent Transparency +// Morgan McGuire, Louis Bavoil - NVIDIA +// Journal of Computer Graphics Techniques (JCGT), vol. 2, no. 2, 122-141, 2013 +// http://jcgt.org/published/0002/02/09/ + +// remark : manage only non colored transmission. Direct implementation of the above paper without +// the suggested extension : +// ... non-refractive colored transmission can be implemented as a simple extension by processing a +// separate coverage +// value per color channel + +// Note, z range from 0 at the camera to +infinity far away ... + +float weight( float z, float alpha ) { + + // pow(alpha, colorResistance) : increase colorResistance if foreground transparent are + // affecting background transparent color clamp(adjust / f(z), min, max) : + // adjust : Range adjustment to avoid saturating at the clamp bounds + // clamp bounds : to be tuned to avoid over or underflow of the reveleage texture. + // f(z) = 1e-5 + pow(z/depthRange, orederingStrength) + // defRange : Depth range over which significant ordering discrimination is required. Here, + // 10 camera space units. + // Decrease if high-opacity surfaces seem “too transparent”, + // increase if distant transparents are blending together too much. + // orderingStrength : Ordering strength. Increase if background is showing through + // foreground too much. + // 1e-5 + ... : avoid dividing by zero ! + + return pow( alpha, 0.5 ) * clamp( 10 / ( 1e-5 + pow( z / 10, 6 ) ), 1e-2, 3 * 1e3 ); +} + +void main() { + vec3 tc = getPerVertexTexCoord(); + // only render non opaque fragments and not fully transparent fragments + vec4 bc = getBaseColor( material, tc ); + // compute the transparency factor + float a = bc.a; + if ( !toDiscard( material, bc ) || a < 0.001 ) discard; + +#ifdef GLTF_MATERIAL_INTERFACE + // Compute the normal closure. Do normal map and capture state about normal and tangent space + NormalInfo nrm_info = getNormalInfo( material.baseMaterial, tc ); + + // Compute the BSDF closure. Compte all view and lighting independant propserties on the + // material + MaterialInfo bsdf_params; + extractBSDFParameters( material, tc, nrm_info, bsdf_params ); + + // get dynamic lighting informations : wiew and light properties + vec3 wo = normalize( in_viewVector ); // outgoing direction + + // Per local light computation + vec3 wi = normalize( in_lightVector ); // incident direction + vec3 lighting = lightContributionFrom( light, getWorldSpacePosition().xyz ); + + // evaluate the BSDF : get a set of layers representing several optical effects + BsdfInfo layers = evaluateBSDF( material, + bsdf_params, + nrm_info, + wi, + wo, + lightContributionFrom( light, getWorldSpacePosition().xyz ) ); + + // Final evaluation of BSDF/layers closure + vec3 color = combineLayers( bsdf_params, layers, nrm_info, wo ); + +#else + // all vectors are in world space + vec3 binormal = getWorldSpaceBiTangent(); + vec3 normalWorld = + getNormal( material, tc, getWorldSpaceNormal(), getWorldSpaceTangent(), binormal ); + vec3 binormalWorld = normalize( cross( normalWorld, getWorldSpaceTangent() ) ); + vec3 tangentWorld = cross( binormalWorld, normalWorld ); + + // A material is always evaluated in the fragment local Frame + // compute matrix from World to local Frame + mat3 world2local; + world2local[0] = vec3( tangentWorld.x, binormalWorld.x, normalWorld.x ); + world2local[1] = vec3( tangentWorld.y, binormalWorld.y, normalWorld.y ); + world2local[2] = vec3( tangentWorld.z, binormalWorld.z, normalWorld.z ); + // transform all vectors in local frame so that N = (0, 0, 1); + vec3 wi = world2local * normalize( in_lightVector ); // incident direction + vec3 wo = world2local * normalize( in_viewVector ); // outgoing direction + + vec3 color = evaluateBSDF( material, tc, wi, wo ) * + lightContributionFrom( light, getWorldSpacePosition().xyz ); +#endif + + float w = weight( gl_FragCoord.z, a ); + f_Accumulation = vec4( color * a, a ) * w; + f_Revealage = vec4( a ); +} diff --git a/src/Dataflow/Rendering/Shaders/WireframeNode/Wireframe.frag.glsl b/src/Dataflow/Rendering/Shaders/WireframeNode/Wireframe.frag.glsl new file mode 100644 index 00000000000..242120d6541 --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/WireframeNode/Wireframe.frag.glsl @@ -0,0 +1,20 @@ +in vec4 gColor; +out vec4 fragColor; +in float pixelWidthDiv2; + +const float PI = 3.1415926535897932384626433832795; + +// AA is one pixel wide after pixelWidth of fully filled. + +float aa( in float dist ) { + float R = sqrt( 2. * .5 * .5 ); + float s = sign( dist ); + float d = max( min( dist - pixelWidthDiv2 + s * R, R ), -R ); + + float theta = 2. * acos( max( min( d / R, 1. ), -1 ) ); + return clamp( R * R / 2. * ( theta - sin( theta ) ) / ( R * R * PI ), 0., 1. ); +} +void main() { + float a = aa( gColor.a ) * aa( -gColor.a ); + fragColor = vec4( vec3( .8, .9, 1. ), a ); +} diff --git a/src/Dataflow/Rendering/Shaders/WireframeNode/Wireframe.geom.glsl b/src/Dataflow/Rendering/Shaders/WireframeNode/Wireframe.geom.glsl new file mode 100644 index 00000000000..3933f78bcdf --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/WireframeNode/Wireframe.geom.glsl @@ -0,0 +1,67 @@ +layout( lines ) in; +layout( triangle_strip, max_vertices = 4 ) out; + +in gl_PerVertex { + vec4 gl_Position; + float gl_PointSize; + float gl_ClipDistance[]; +} +gl_in[]; + +out gl_PerVertex { + vec4 gl_Position; + float gl_PointSize; + float gl_ClipDistance[]; +}; + +in vec4 vPosition[2]; +// in vec4 vColor[2]; + +out vec4 gColor; +out float pixelWidthDiv2; +uniform vec2 viewport; +vec4 vColor[2]; + +void main() { + vec3 vp = vec3( viewport, 1. ); + + // clip space + vec4 css0 = gl_in[0].gl_Position; + vec4 css1 = gl_in[1].gl_Position; + + // coherent clip space + vec4 scss0 = css0 * css1.w; + vec4 scss1 = css1 * css0.w; + + vec3 dir = ( scss1 - scss0 ).xyz; + float pixelWidth = 1.8; + const float border = 4.; + vec3 slope = normalize( vec3( -dir.y, dir.x, 0 ) ); + vec4 n = vec4( vec3( pixelWidth + border ) / vp * slope, 0 ); + + vec4 a = vec4( ( scss0 + n * scss0.w ) ); + vec4 b = vec4( ( scss0 - n * scss0.w ) ); + vec4 c = vec4( ( scss1 + n * scss0.w ) ); + vec4 d = vec4( ( scss1 - n * scss0.w ) ); + pixelWidthDiv2 = pixelWidth / 2.; + vColor[0] = vec4( vec3( .7 ), -pixelWidthDiv2 - border / 2. ); + vColor[1] = vec4( vec3( .7 ), +pixelWidthDiv2 + border / 2. ); + + gColor = vColor[0]; + gl_Position = a; + EmitVertex(); + + gColor = vColor[1]; + gl_Position = b; + EmitVertex(); + + gColor = vColor[0]; + gl_Position = c; + EmitVertex(); + + gColor = vColor[1]; + gl_Position = d; + EmitVertex(); + + EndPrimitive(); +} diff --git a/src/Dataflow/Rendering/Shaders/WireframeNode/Wireframe.vert.glsl b/src/Dataflow/Rendering/Shaders/WireframeNode/Wireframe.vert.glsl new file mode 100644 index 00000000000..c68593cb2a2 --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/WireframeNode/Wireframe.vert.glsl @@ -0,0 +1,13 @@ +#include "TransformStructs.glsl" + +layout( location = 0 ) in vec3 in_position; +out vec4 vPosition; + +uniform Transform transform; + +void main() { + mat4 mvp = transform.proj * transform.view * transform.model; + vec4 pos = mvp * vec4( in_position.xyz, 1.0 ); + gl_Position = pos; + vPosition = pos; +} diff --git a/src/Dataflow/Rendering/Shaders/shader.vert.glsl b/src/Dataflow/Rendering/Shaders/shader.vert.glsl new file mode 100644 index 00000000000..34610cd1f0d --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/shader.vert.glsl @@ -0,0 +1,39 @@ +#include "TransformStructs.glsl" +#include "DefaultLight.glsl" + +layout (location = 0) in vec3 in_position; +layout (location = 1) in vec3 in_normal; +layout (location = 2) in vec3 in_tangent; +layout (location = 3) in vec3 in_bitangent; +layout (location = 4) in vec3 in_texcoord; +layout (location = 5) in vec4 in_color; + +uniform Transform transform; + +layout (location = 0) out vec3 out_position;// Position in World Space +layout (location = 1) out vec3 out_normal; +layout (location = 2) out vec3 out_texcoord;// Used in case of normal mapping +layout (location = 3) out vec3 out_vertexcolor; +layout (location = 4) out vec3 out_tangent; +layout (location = 5) out vec3 out_viewVector; +layout (location = 6) out vec3 out_lightVector; + + +void main() +{ + mat4 mvp = transform.proj * transform.view * transform.model; + gl_Position = mvp * vec4(in_position, 1.0); + + vec4 pos = transform.model * vec4(in_position, 1.0); + pos /= pos.w; + + vec3 eye = -transform.view[3].xyz * mat3(transform.view); + + out_position = vec3(pos); + out_texcoord = in_texcoord; + out_normal = normalize(mat3(transform.worldNormal) * in_normal); + out_tangent = normalize(mat3(transform.model) * in_tangent); + out_viewVector = normalize(eye - pos.xyz); + out_lightVector = getLightDirection(light, pos.xyz); + out_vertexcolor = in_color.rgb; +} diff --git a/src/Dataflow/Rendering/Shaders/shader_nolight.vert.glsl b/src/Dataflow/Rendering/Shaders/shader_nolight.vert.glsl new file mode 100644 index 00000000000..a7a86f018e0 --- /dev/null +++ b/src/Dataflow/Rendering/Shaders/shader_nolight.vert.glsl @@ -0,0 +1,38 @@ +#include "TransformStructs.glsl" + +layout (location = 0) in vec3 in_position; +layout (location = 1) in vec3 in_normal; +layout (location = 2) in vec3 in_tangent; +layout (location = 3) in vec3 in_bitangent; +layout (location = 4) in vec3 in_texcoord; +layout (location = 5) in vec4 in_color; + +uniform Transform transform; + +layout (location = 0) out vec3 out_position; +layout (location = 1) out vec3 out_normal; +layout (location = 2) out vec3 out_texcoord; +layout (location = 3) out vec3 out_vertexcolor; +layout (location = 4) out vec3 out_tangent; +layout (location = 5) out vec3 out_viewVector; + +void main() +{ + mat4 mvp = transform.proj * transform.view * transform.model; + gl_Position = mvp * vec4(in_position, 1.0); + + vec4 pos = transform.model * vec4(in_position, 1.0); + pos /= pos.w; + + vec3 normal = mat3(transform.worldNormal) * in_normal; + vec3 tangent = mat3(transform.model) * in_tangent; + + vec3 eye = -transform.view[3].xyz * mat3(transform.view); + + out_position = vec3(pos); + out_texcoord = in_texcoord; + out_normal = normal; + out_tangent = tangent; + out_vertexcolor = in_color.rgb; + out_viewVector = normalize(eye - pos.xyz); +} diff --git a/src/Dataflow/Rendering/filelist.cmake b/src/Dataflow/Rendering/filelist.cmake new file mode 100644 index 00000000000..c277bfa8f09 --- /dev/null +++ b/src/Dataflow/Rendering/filelist.cmake @@ -0,0 +1,48 @@ +# ---------------------------------------------------- +# This file should not be generated by the radium script +# ---------------------------------------------------- + +set(dataflow_rendering_sources + Renderer/ControllableRenderer.cpp + Renderer/RenderGraphController.cpp + RenderingGraph.cpp + Nodes/AntiAliasing/FxaaNode.cpp + Nodes/RenderingBuiltInsNodes.cpp + Nodes/Sinks/DisplaySinkNode.cpp + Nodes/Sources/TextureSourceNode.cpp + Nodes/RenderNodes/ClearColorNode.cpp + Nodes/RenderNodes/EmissivityRenderNode.cpp + Nodes/RenderNodes/EnvironmentLightingNode.cpp + Nodes/RenderNodes/GeometryAovsNode.cpp + Nodes/RenderNodes/LocalLightingNode.cpp + Nodes/RenderNodes/SimpleRenderNode.cpp + Nodes/RenderNodes/SsaoRenderNode.cpp + Nodes/RenderNodes/TransparentLocalLightingNode.cpp + Nodes/RenderNodes/VolumeLocalLightingNode.cpp + Nodes/RenderNodes/WireframeRenderingNode.cpp +) + +set(dataflow_rendering_headers + Nodes/RenderingBuiltInsNodes.hpp + Nodes/RenderingNode.hpp + Nodes/AntiAliasing/FxaaNode.hpp + Nodes/RenderNodes/ClearColorNode.hpp + Nodes/RenderNodes/EmissivityRenderNode.hpp + Nodes/RenderNodes/EnvironmentLightingNode.hpp + Nodes/RenderNodes/GeometryAovsNode.hpp + Nodes/RenderNodes/LocalLightingNode.hpp + Nodes/RenderNodes/SimpleRenderNode.hpp + Nodes/RenderNodes/SsaoRenderNode.hpp + Nodes/RenderNodes/TransparentLocalLightingNode.hpp + Nodes/RenderNodes/VolumeLocalLightingNode.hpp + Nodes/RenderNodes/WireframeRenderingNode.hpp + Nodes/Sinks/DisplaySinkNode.hpp + Nodes/Sources/Scene.hpp + Nodes/Sources/EnvMapSourceNode.hpp + Nodes/Sources/TextureSourceNode.hpp + Renderer/ControllableRenderer.hpp + Renderer/RenderGraphController.hpp + RenderingGraph.hpp +) + +set(dataflow_rendering_resources Shaders) diff --git a/src/Dataflow/Rendering/pch.hpp b/src/Dataflow/Rendering/pch.hpp new file mode 100644 index 00000000000..6f70f09beec --- /dev/null +++ b/src/Dataflow/Rendering/pch.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/src/Engine/Data/EnvironmentTexture.cpp b/src/Engine/Data/EnvironmentTexture.cpp index 3fda194fe83..f85f56458da 100644 --- a/src/Engine/Data/EnvironmentTexture.cpp +++ b/src/Engine/Data/EnvironmentTexture.cpp @@ -425,13 +425,14 @@ void EnvironmentTexture::setupTexturesFromSphericalEquiRectangular() { Scalar v = -1 + j * duv; Vector3 d = bases[imgIdx][0] + u * bases[imgIdx][1] + v * bases[imgIdx][2]; d = d.normalized(); - Vector2 st { w * sphericalPhi( d ) / ( 2 * M_PI ), h * sphericalTheta( d ) / M_PI }; + Vector2 st { w * sphericalPhi( d ) / ( 2_ra * M_PI ), + h * sphericalTheta( d ) / M_PI }; // TODO : use st to access and filter the original envmap // for now, no filtering is done. (eq to GL_NEAREST) - int s = int( st.x() ); - int t = int( st.y() ); - int cu = int( ( u / 2 + 0.5 ) * textureSize ); - int cv = int( ( v / 2 + 0.5 ) * textureSize ); + int s = std::min( int( st.x() ), w - 1 ); + int t = std::min( int( st.y() ), h - 1 ); + int cu = int( ( u / 2_ra + 0.5_ra ) * textureSize ); + int cv = int( ( v / 2_ra + 0.5_ra ) * textureSize ); m_skyData[imgIdx][4 * ( cv * textureSize + cu ) + 0] = latlonPix[4 * ( t * w + s ) + 0]; @@ -664,10 +665,11 @@ void EnvironmentTexture::updateGL() { "in vec3 incidentDirection;\n" "uniform samplerCube skyTexture;\n" "uniform float strength;\n" + "uniform float alpha;\n" "void main(void)\n" "{\n" " vec3 envColor = texture(skyTexture, normalize(incidentDirection)).rgb;\n" - " outColor =vec4(strength*envColor, 1);\n" + " outColor =vec4(strength*envColor, alpha); \n" "}\n" }; Ra::Engine::Data::ShaderConfiguration config { "EnvironmentTexture::Builtin SkyBox" }; config.addShaderSource( Ra::Engine::Data::ShaderType::ShaderType_VERTEX, @@ -688,7 +690,8 @@ void EnvironmentTexture::updateGL() { } } -void EnvironmentTexture::render( const Ra::Engine::Data::ViewingParameters& viewParams ) { +void EnvironmentTexture::render( const Ra::Engine::Data::ViewingParameters& viewParams, + bool asOpaque ) { if ( m_isSkyBox ) { // put this in a initializeGL method ? if ( !m_glReady ) { updateGL(); } @@ -709,6 +712,8 @@ void EnvironmentTexture::render( const Ra::Engine::Data::ViewingParameters& view m_skyTexture->bind( 0 ); m_skyShader->setUniform( "skytexture", 0 ); m_skyShader->setUniform( "strength", m_environmentStrength ); + if ( asOpaque ) { m_skyShader->setUniform( "alpha", float( 1 ) ); } + else { m_skyShader->setUniform( "alpha", float( 0 ) ); } GLboolean depthEnabled; glGetBooleanv( GL_DEPTH_WRITEMASK, &depthEnabled ); glDepthMask( GL_FALSE ); diff --git a/src/Engine/Data/EnvironmentTexture.hpp b/src/Engine/Data/EnvironmentTexture.hpp index 9f71f1a242c..af81738d542 100644 --- a/src/Engine/Data/EnvironmentTexture.hpp +++ b/src/Engine/Data/EnvironmentTexture.hpp @@ -106,7 +106,7 @@ class RA_ENGINE_API EnvironmentTexture * \brief Render the envmap as a textured cube. This method does nothing if the envmap is not a * skybox \param viewParams The viewing parameter used to draw the scene */ - void render( const Ra::Engine::Data::ViewingParameters& viewParams ); + void render( const Ra::Engine::Data::ViewingParameters& viewParams, bool asOpaque = true ); /** * \brief Set the state of the skybox diff --git a/src/Engine/Data/SphereSampler.cpp b/src/Engine/Data/SphereSampler.cpp new file mode 100644 index 00000000000..7a9725357cf --- /dev/null +++ b/src/Engine/Data/SphereSampler.cpp @@ -0,0 +1,76 @@ +#include +#include + +namespace Ra { +namespace Engine { +namespace Data { + +using namespace Core::Random; + +SphereSampler::SphereSampler( SamplingMethod method, int level ) { + std::function generator; + switch ( method ) { + case SamplingMethod::FIBONACCI: { + auto g = SphericalPointSet( level ); + m_nbPoints = g.range(); + generator = std::move( g ); + } break; + case SamplingMethod::HAMMERSLEY: { + auto g = SphericalPointSet( level ); + m_nbPoints = g.range(); + generator = std::move( g ); + } break; + case SamplingMethod::RANDOM: { + auto g = SphericalPointSet( level ); + m_nbPoints = g.range(); + generator = std::move( g ); + } break; + case SamplingMethod::GEODESIC: + // not yet implemented, resolve as default + // default: // for the moment, implement GEODESIC + { + auto g = SphericalPointSet( level ); + m_nbPoints = g.range(); + generator = std::move( g ); + } + break; + } + + std::mt19937 seq( 0 ); + std::uniform_real_distribution random( 0._ra, 1._ra ); + + m_points.reserve( size_t( m_nbPoints ) ); + for ( auto i = 0; i < m_nbPoints; ++i ) { + auto pt = generator( i ); + auto d = random( seq ); + // lerp will only be available on C++20 and after ... + // d = std::lerp(0.1, 1.0, d*d); + d *= d; + d = 0.1_ra + d * 0.9_ra; + m_points.emplace_back( pt[0], pt[1], pt[2], d ); + } +} + +Ra::Engine::Data::Texture* SphereSampler::asTexture() { + using namespace gl; + if ( m_texture != nullptr ) { return m_texture.get(); } + + Ra::Engine::Data::TextureParameters texparams; + texparams.name = "Directional samples"; + texparams.width = size_t( m_nbPoints ); + texparams.height = 1; + texparams.target = GL_TEXTURE_RECTANGLE; + texparams.minFilter = GL_NEAREST; + texparams.magFilter = GL_NEAREST; + texparams.internalFormat = GL_RGBA32F; + texparams.format = GL_RGBA; + texparams.type = GL_FLOAT; + texparams.texels = m_points.data(); + m_texture = std::make_unique( texparams ); + m_texture->initializeGL(); + return m_texture.get(); +} + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/Data/SphereSampler.hpp b/src/Engine/Data/SphereSampler.hpp new file mode 100644 index 00000000000..f39987e777c --- /dev/null +++ b/src/Engine/Data/SphereSampler.hpp @@ -0,0 +1,52 @@ +#pragma once +#include + +#include +#include + +#include + +namespace Ra { +namespace Engine { +namespace Data { + +/// Sample the volume of a sphere as a random displacement of its sampled surface +class RA_ENGINE_API SphereSampler +{ + public: + /// Available samplers + enum class SamplingMethod { + FIBONACCI, ///<- Sample the sphere with a fibonacci sequence + HAMMERSLEY, ///<- Sample the sphere using Hammersley Point Set + RANDOM, ///<-- Sample the sphere using uniform random sequence + GEODESIC, ///<- Sample the sphere by subdivision of an icosahedron + }; + /// Generate a sequence of point distributed on the sphere + /// @param method the sampling method to use (@see SamplingMethod) + /// @param level the number of point to generate + explicit SphereSampler( SamplingMethod method, int level = 0 ); + SphereSampler( const SphereSampler& ) = delete; + SphereSampler& operator=( const SphereSampler& ) = delete; + SphereSampler( SphereSampler&& ) = delete; + SphereSampler& operator=( SphereSampler&& ) = delete; + /// destructor + ~SphereSampler() = default; + + /// Get the sampling scheme as a Radium texture + Ra::Engine::Data::Texture* asTexture(); + + /// Return the number of samples generated + [[nodiscard]] int nbSamples() const { return m_nbPoints; } + + private: + /// Texture generated on demand to be used with an OpenGL shader + std::unique_ptr m_texture { nullptr }; + /// Final number of points + int m_nbPoints; + /// The points on the sphere + Ra::Core::VectorArray m_points; +}; + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/filelist.cmake b/src/Engine/filelist.cmake index 274f82178ba..a0923b050ea 100644 --- a/src/Engine/filelist.cmake +++ b/src/Engine/filelist.cmake @@ -20,6 +20,7 @@ set(engine_sources Data/ShaderProgram.cpp Data/ShaderProgramManager.cpp Data/SimpleMaterial.cpp + Data/SphereSampler.cpp Data/Texture.cpp Data/TextureManager.cpp Data/VolumeObject.cpp @@ -73,6 +74,7 @@ set(engine_headers Data/ShaderProgram.hpp Data/ShaderProgramManager.hpp Data/SimpleMaterial.hpp + Data/SphereSampler.hpp Data/Texture.hpp Data/TextureManager.hpp Data/ViewingParameters.hpp diff --git a/src/Gui/BaseApplication.cpp b/src/Gui/BaseApplication.cpp index 8681c07ab92..a1e09c0a812 100644 --- a/src/Gui/BaseApplication.cpp +++ b/src/Gui/BaseApplication.cpp @@ -309,6 +309,13 @@ void BaseApplication::initialize( const WindowFactory& factory, pluginsPath, parser.values( "loadPlugin" ), parser.values( "ignorePlugin" ) ) ) { LOG( logDEBUG ) << "No plugin found in default path " << pluginsPath; } + // if a plugin path was set on the command line, load plugins from this path + if ( !m_pluginPath.empty() ) { + if ( !loadPlugins( + m_pluginPath, parser.values( "loadPlugin" ), parser.values( "ignorePlugin" ) ) ) { + LOG( logDEBUG ) << "No plugin found in command line given plugin path " << m_pluginPath; + } + } // load supplemental plugins { QSettings settings; diff --git a/src/Gui/Widgets/ControlPanel.cpp b/src/Gui/Widgets/ControlPanel.cpp index cca0783f5fd..63fd127068e 100644 --- a/src/Gui/Widgets/ControlPanel.cpp +++ b/src/Gui/Widgets/ControlPanel.cpp @@ -158,7 +158,8 @@ void ControlPanel::addColorInput( Ra::Core::Utils::Color color, bool withAlpha, const std::string& tooltip ) { - auto button = new QPushButton( name.c_str(), this ); + auto button = new QPushButton( name.c_str(), this ); + button->setObjectName( name.c_str() ); auto srgbColor = Ra::Core::Utils::Color::linearRGBTosRGB( color ); auto clrBttn = QColor::fromRgbF( srgbColor[0], srgbColor[1], srgbColor[2], srgbColor[3] ); auto clrDlg = [callback, clrBttn, withAlpha, button, name]() mutable { diff --git a/tests/unittest/CMakeLists.txt b/tests/unittest/CMakeLists.txt index aa26cd009d9..062c06f2969 100644 --- a/tests/unittest/CMakeLists.txt +++ b/tests/unittest/CMakeLists.txt @@ -12,7 +12,7 @@ set(test_src Core/camera.cpp Core/color.cpp Core/containers.cpp - Core/demangle.cpp + Core/typeutils.cpp Core/distance.cpp Core/enumconverter.cpp Core/geometryData.cpp @@ -22,6 +22,7 @@ set(test_src Core/obb.cpp Core/observer.cpp Core/polyline.cpp + Core/random.cpp Core/raycast.cpp Core/resources.cpp Core/string.cpp @@ -30,6 +31,12 @@ set(test_src Core/topomesh.cpp Core/variableset.cpp Core/vectorarray.cpp + Dataflow/customnodes.cpp + Dataflow/graph.cpp + Dataflow/nodes.cpp + Dataflow/renderinggraph.cpp + Dataflow/serialization.cpp + Dataflow/sourcesandsinks.cpp Engine/environmentmap.cpp Engine/renderparameters.cpp Engine/signalmanager.cpp @@ -54,7 +61,7 @@ target_include_directories(unittests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_options(unittests PUBLIC ${RA_DEFAULT_COMPILE_OPTIONS}) target_compile_definitions(unittests PRIVATE UNIT_TESTS) # add -DUNIT_TESTS define -target_link_libraries(unittests PRIVATE Catch2::Catch2 Core Engine Gui) +target_link_libraries(unittests PRIVATE Catch2::Catch2 Core Engine Gui Dataflow) add_dependencies(unittests Catch2 Core Engine Gui) find_package(Filesystem COMPONENTS Final Experimental REQUIRED) diff --git a/tests/unittest/Core/demangle.cpp b/tests/unittest/Core/demangle.cpp deleted file mode 100644 index da3cc5c2073..00000000000 --- a/tests/unittest/Core/demangle.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include -#include - -#include -#include - -#include - -namespace TypeTests { -struct TypeName_struct {}; -} // namespace TypeTests -TEST_CASE( "Core/Utils/TypesUtils", "[Core][Core/Utils][TypesUtils]" ) { - SECTION( "Demangle from typename" ) { - using Ra::Core::Utils::demangleType; - - REQUIRE( std::string( demangleType() ) == "int" ); - REQUIRE( std::string( demangleType() ) == "float" ); - REQUIRE( std::string( demangleType() ) == "unsigned int" ); - // TODO, verify type demangling on windows -#ifndef _WIN32 - REQUIRE( std::string( demangleType() ) == "unsigned long" ); -#else - REQUIRE( std::string( demangleType() ) == "unsigned __int64" ); -#endif - auto demangledName = std::string( demangleType>() ); - REQUIRE( demangledName == "std::vector>" ); - - demangledName = std::string( demangleType() ); - REQUIRE( demangledName == "TypeTests::TypeName_struct" ); - } - - SECTION( "Demangle from instance" ) { - using Ra::Core::Utils::demangleType; - - int i { 1 }; - float f { 2 }; - unsigned int u { 3 }; - size_t s { 4 }; - - REQUIRE( std::string( demangleType( i ) ) == "int" ); - REQUIRE( std::string( demangleType( f ) ) == "float" ); - REQUIRE( std::string( demangleType( u ) ) == "unsigned int" ); - // TODO, verify type demangling on windows -#ifndef _WIN32 - REQUIRE( std::string( demangleType( s ) ) == "unsigned long" ); -#else - REQUIRE( std::string( demangleType( s ) ) == "unsigned __int64" ); -#endif - std::vector v; - auto demangledName = std::string( demangleType( v ) ); - REQUIRE( demangledName == "std::vector>" ); - - TypeTests::TypeName_struct tns; - demangledName = std::string( demangleType( tns ) ); - REQUIRE( demangledName == "TypeTests::TypeName_struct" ); - } -} diff --git a/tests/unittest/Core/random.cpp b/tests/unittest/Core/random.cpp new file mode 100644 index 00000000000..2c302457f8e --- /dev/null +++ b/tests/unittest/Core/random.cpp @@ -0,0 +1,106 @@ +#include +#include + +#include +#include + +#include +using namespace Ra::Core::Random; + +TEST_CASE( "Core/Random/RandomPointSet", "[Core][Core/Random][PointSet]" ) { + SECTION( "Fibonacci sequence" ) { + std::array fib_verif { 0_ra, + 0.61803398874989479150343640867504_ra, + 1.2360679774997895830068728173501_ra, + 1.8541019662496844855326116885408_ra, + 2.4721359549995791660137456347002_ra }; + FibonacciSequence fib { 2 }; + // Our fibonacci sequence is only defined for more than 5 points + REQUIRE( fib.range() == 5 ); + for ( size_t i = 0; i < 5; ++i ) { + REQUIRE( isApprox( fib( i ), fib_verif[i] ) ); + } + } + + SECTION( "VanDerCorput sequence" ) { + std::array vdc_verif { 0_ra, 0.5_ra, 0.25_ra, 0.75_ra, 0.125_ra }; + VanDerCorputSequence vdc; + for ( size_t i = 0; i < 5; ++i ) { + REQUIRE( isApprox( vdc( i ), vdc_verif[i] ) ); + } + } + + SECTION( "Fibonacci point set" ) { + std::array, 5> fibseq_verif { + std::pair { 0_ra, 0_ra / 5_ra }, + { 0.61803398874989479150343640867504_ra, 1_ra / 5_ra }, + { 1.2360679774997895830068728173501_ra, 2_ra / 5_ra }, + { 1.8541019662496844855326116885408_ra, 3_ra / 5_ra }, + { 2.4721359549995791660137456347002_ra, 4_ra / 5_ra } }; + FibonacciPointSet fibs { 5 }; + for ( size_t i = 0; i < 5; ++i ) { + auto v = fibs( i ); + REQUIRE( isApprox( v[0], fibseq_verif[i].first ) ); + REQUIRE( isApprox( v[1], fibseq_verif[i].second ) ); + } + } + + SECTION( "Hammersley point set" ) { + std::array, 5> seq_verif { + std::pair { 0_ra, 0_ra / 5_ra }, + { 1_ra / 5_ra, 0.5_ra }, + { 2_ra / 5_ra, 0.25_ra }, + { 3_ra / 5_ra, 0.75_ra }, + { 4_ra / 5_ra, 0.125_ra } }; + HammersleyPointSet seq { 5 }; + for ( size_t i = 0; i < 5; ++i ) { + auto v = seq( i ); + REQUIRE( isApprox( v[0], seq_verif[i].first ) ); + REQUIRE( isApprox( v[1], seq_verif[i].second ) ); + } + } + + // todo, verify if the sequence is always the same (it should) on any systems/run + SECTION( "MersenneTwister point set" ) { +#ifdef CORE_USE_DOUBLE + // Sequence valid only when Scalar == double + std::array, 5> seq_verif { + std::pair { 0.59284461651668263204584263803554_ra, + 0.84426574425659828282419994138763_ra }, + { 0.85794561998982987738315841852454_ra, 0.84725173738433123826752080276492_ra }, + { 0.62356369649610832173181051985011_ra, 0.38438170837375662536317122430773_ra }, + { 0.29753460535723419422282631785492_ra, 0.056712975933163663200264892338964_ra }, + { 0.27265629474158931122573790162278_ra, 0.47766511174464632016878340436961_ra } }; +#else + // Sequence valid only when Scalar == float + std::array, 5> seq_verif { + std::pair { 0.548813521862030029296875_ra, + 0.59284460544586181640625_ra }, + { 0.71518933773040771484375_ra, 0.844265758991241455078125_ra }, + { 0.602763354778289794921875_ra, 0.857945621013641357421875_ra }, + { 0.544883191585540771484375_ra, 0.847251713275909423828125_ra }, + { 0.4236547946929931640625_ra, 0.623563706874847412109375_ra } }; +#endif + MersenneTwisterPointSet seq { 5 }; + for ( size_t i = 0; i < 5; ++i ) { + auto v = seq( i ); + REQUIRE( isApprox( v[0], seq_verif[i].first ) ); + REQUIRE( isApprox( v[1], seq_verif[i].second ) ); + } + } + + SECTION( "SphericalPointSet point set (Hammersley)" ) { + std::array, 5> seq_verif { + std::pair { 1.2246467991473532071737640294584e-16_ra, 0_ra }, + { 0.30901699437494745126286943559535_ra, 0.95105651629515353118193843329209_ra }, + { -0.70062926922203661028731858095853_ra, 0.50903696045512725198989301134134_ra }, + { -0.70062926922203672130962104347418_ra, -0.50903696045512702994528808631003_ra }, + { 0.20439552950218897731105016646325_ra, -0.62906475622110624712490789534058_ra } }; + SphericalPointSet seq { 5 }; + for ( size_t i = 0; i < 5; ++i ) { + auto v = seq( i ); + REQUIRE( isApprox( v[0], seq_verif[i].first ) ); + REQUIRE( isApprox( v[1], seq_verif[i].second ) ); + } + } +} diff --git a/tests/unittest/Core/typeutils.cpp b/tests/unittest/Core/typeutils.cpp new file mode 100644 index 00000000000..12e4633fdda --- /dev/null +++ b/tests/unittest/Core/typeutils.cpp @@ -0,0 +1,68 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace TypeTests { +struct TypeName_struct {}; +} // namespace TypeTests +TEST_CASE( "Core/Utils/TypesUtils", "[Core][Utils][TypesUtils]" ) { + SECTION( "Demangle from typename" ) { + using Ra::Core::Utils::demangleType; + + REQUIRE( demangleType() == "int" ); + REQUIRE( demangleType() == "float" ); + REQUIRE( demangleType() == "unsigned int" ); + REQUIRE( demangleType() == "unsigned long" ); + + auto demangledName = demangleType>(); + REQUIRE( demangledName == "std::vector>" ); + + demangledName = demangleType(); + REQUIRE( demangledName == "TypeTests::TypeName_struct" ); + + demangledName = demangleType( std::type_index( typeid( std::vector ) ) ); + REQUIRE( demangledName == "std::vector>" ); + } + + SECTION( "Demangle from instance" ) { + using Ra::Core::Utils::demangleType; + + int i { 1 }; + float f { 2 }; + unsigned int u { 3 }; + size_t s { 4 }; + + REQUIRE( demangleType( i ) == "int" ); + REQUIRE( demangleType( f ) == "float" ); + REQUIRE( demangleType( u ) == "unsigned int" ); + REQUIRE( demangleType( s ) == "unsigned long" ); + +#ifndef _WIN32 + // this segfault on windows due to out_of_bound exception. why ??? + std::vector v; + auto demangledName = demangleType( v ); + REQUIRE( demangledName == "std::vector>" ); +#endif + TypeTests::TypeName_struct tns; + auto demangledNameFromStruct = demangleType( tns ); + REQUIRE( demangledNameFromStruct == "TypeTests::TypeName_struct" ); + } + + SECTION( "Type traits" ) { + using namespace Ra::Core::Utils; + REQUIRE( is_container::value == false ); + REQUIRE( is_container::value == false ); + REQUIRE( is_container::value == false ); + REQUIRE( is_container>::value == true ); + REQUIRE( is_container>::value == true ); + REQUIRE( is_container>::value == true ); + REQUIRE( is_container>::value == true ); + } +} diff --git a/tests/unittest/Dataflow/customnodes.cpp b/tests/unittest/Dataflow/customnodes.cpp new file mode 100644 index 00000000000..9f63934de7a --- /dev/null +++ b/tests/unittest/Dataflow/customnodes.cpp @@ -0,0 +1,346 @@ +/** + * Demonstrate how to define custom nodes anduse factory to serialize graphs with custom nodes + */ +#include + +#include +#include + +#include + +#include + +#include +#include +#include +#include + +using namespace Ra::Dataflow::Core; + +namespace Customs { +using CustomStringSource = Sources::SingleDataSourceNode; +using CustomStringSink = Sinks::SinkNode; + +//! [Develop a custom node] +/** + * \brief generate a predicate that compare a value wrt a threshold. + * The name of the operator is fetched from input port "name" or the internal data set using + * setFunctionName. Available operator are "=", ">", ">=", "<", "<=", "!=", "true", "false". + * + * The threshold is fetched from input port "threshold" or the internal data set using setThreshold. + * + * The operator is sent on the output port "f". + * + * \tparam T the type of the parameter to evaluate + */ +template +class FilterSelector final : public Node +{ + public: + using function_type = std::function; + + explicit FilterSelector( const std::string& name ) : FilterSelector( name, getTypename() ) {} + + bool execute() override { + // get operator parameters + if ( m_portName->isLinked() ) { m_operatorName = m_portName->getData(); } + if ( m_portThreshold->isLinked() ) { m_threshold = m_portThreshold->getData(); } + // compute the result associated to the output port + m_currentFunction = m_functions[m_operatorName]; + return true; + } + + /** \brief Set the function name used to select the function to deliver. + * this name will be used if the input port "name" is not linked + * @param name + */ + void setOperatorName( const std::string& name ) { m_operatorName = name; } + /** + * \brief Get the delivered data + * @return The non owning pointer (alias) to the delivered data. + */ + function_type* getOperator() const { return m_functions[m_operatorName]; } + + /** \brief Set the threshold - will copy the value into the node + * @param name + */ + void setThreshold( const T& t ) { m_threshold = t; } + /** \brief Get the threshold + */ + T getThreshold() const { return m_threshold; } + + protected: + bool fromJsonInternal( const nlohmann::json& data ) override { + if ( data.contains( "operator" ) ) { m_operatorName = data["operator"]; } + else { m_operatorName = "true"; } + if ( data.contains( "threshold" ) ) { m_threshold = data["threshold"]; } + else { m_threshold = T {}; } + return true; + } + + void toJsonInternal( nlohmann::json& data ) const override { + data["operator"] = m_operatorName; + data["threshold"] = m_threshold; + } + + public: + static const std::string& getTypename() { + static std::string demangledTypeName = std::string { "FilterSelector<" } + + Ra::Dataflow::Core::simplifiedDemangledType() + + ">"; + return demangledTypeName; + } + + private: + FilterSelector( const std::string& instanceName, const std::string& typeName ) : + Node( instanceName, typeName ) { + // Adding ports to node + addInput( m_portName ); + addInput( m_portThreshold ); + addOutput( m_operatourOut, &m_currentFunction ); + addOutput( m_nameOut, &m_operatorName ); + } + + /// Alias to the output port + PortOut* m_operatourOut { new PortOut( "f", this ) }; + PortOut* m_nameOut { new PortOut( "name", this ) }; + /// Alias for the input ports + PortIn* m_portName { new PortIn( "name", this ) }; + PortIn* m_portThreshold { new PortIn( "threshold", this ) }; + + /// The data provided by the node + std::map m_functions { + { "true", []( const T& ) { return true; } }, + { "false", []( const T& ) { return false; } }, + { "<", [this]( const T& v ) { return v < this->m_threshold; } }, + { ">", [this]( const T& v ) { return v > this->m_threshold; } } }; + + std::string m_operatorName { "true" }; + function_type m_currentFunction = m_functions[m_operatorName]; + T m_threshold {}; +}; +//! [Develop a custom node] +} // namespace Customs + +// Reusable function to create a graph +template +DataflowGraph* buildgraph( const std::string& name ) { + auto g = new DataflowGraph( name ); + + auto addedNode = g->addNode( + std::make_unique>>( "ds" ) ); + REQUIRE( addedNode.first ); + auto ds = addedNode.second; + + addedNode = + g->addNode( std::make_unique>>( "rs" ) ); + REQUIRE( addedNode.first ); + auto rs = addedNode.second; + + addedNode = g->addNode( std::make_unique>( "ts" ) ); + REQUIRE( addedNode.first ); + auto ts = addedNode.second; + + addedNode = g->addNode( std::make_unique( "ss" ) ); + REQUIRE( addedNode.first ); + auto ss = addedNode.second; + + addedNode = g->addNode( std::make_unique( "nm" ) ); + REQUIRE( addedNode.first ); + auto nm = addedNode.second; + + addedNode = g->addNode( std::make_unique>( "fs" ) ); + REQUIRE( addedNode.first ); + auto fs = addedNode.second; + + addedNode = g->addNode( + std::make_unique>>( "fl" ) ); + REQUIRE( addedNode.first ); + auto fl = addedNode.second; + + bool ok; + ok = g->addLink( ds, "to", fl, "in" ); + REQUIRE( ok ); + ok = g->addLink( fl, "out", rs, "from" ); + REQUIRE( ok ); + ok = g->addLink( ss, "to", fs, "name" ); + REQUIRE( ok ); + ok = g->addLink( ts, "to", fs, "threshold" ); + REQUIRE( ok ); + ok = g->addLink( fs, "f", fl, "f" ); + REQUIRE( ok ); + ok = g->addLink( fs, "name", nm, "from" ); + REQUIRE( ok ); + return g; +} + +// test sections +TEST_CASE( "Dataflow/Core/Custom nodes", "[Dataflow][Core][Custom nodes]" ) { + SECTION( "Build graph with custom nodes" ) { + // build a graph + auto g = buildgraph( "testCustomNodes" ); + + // get input and ouput of the graph + auto inputCollection = g->getDataSetter( "ds_to" ); + REQUIRE( inputCollection != nullptr ); + auto inputOpName = g->getDataSetter( "ss_to" ); + REQUIRE( inputOpName != nullptr ); + auto inputThreshold = g->getDataSetter( "ts_to" ); + REQUIRE( inputThreshold != nullptr ); + + auto filteredCollection = g->getDataGetter( "rs_from" ); + REQUIRE( filteredCollection != nullptr ); + auto generatedOperator = g->getDataGetter( "nm_from" ); + REQUIRE( generatedOperator != nullptr ); + + // parameterize the graph + using CollectionType = Ra::Core::VectorArray; + CollectionType testVector; + testVector.reserve( 10 ); + std::mt19937 gen( 0 ); + std::uniform_real_distribution dis( 0.0_ra, 1.0_ra ); + // Fill the vector with random numbers between 0 and 1 + for ( size_t n = 0; n < testVector.capacity(); ++n ) { + testVector.push_back( dis( gen ) ); + } + inputCollection->setData( &testVector ); + + Scalar threshold { 0.5_ra }; + inputThreshold->setData( &threshold ); + + std::string op { "true" }; + inputOpName->setData( &op ); + + std::cout << "Data sent to graph : \n\toperator " << op << " : \n\t"; + for ( auto ord : testVector ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + + // execute the graph that filter out nothing + // execute + auto r = g->execute(); + REQUIRE( r ); + + // Getters are usable only after successful compilation/execution of the graph + // Get results as references (no need to get them again later if the graph does not change) + auto& vres = filteredCollection->getData(); + auto& vop = generatedOperator->getData(); + + REQUIRE( vop == "true" ); + REQUIRE( vres.size() == testVector.size() ); + std::cout << "Result after applying operator " << vop << " (from " << op + << " ) and threshold " << threshold << ": \n\t"; + for ( auto ord : vres ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + + // change operator to filter out everything + op = "false"; + r = g->execute(); + REQUIRE( r ); + REQUIRE( vop == "false" ); + REQUIRE( vres.size() == 0 ); + + std::cout << "Result after applying operator " << vop << " (from " << op + << " ) and threshold " << threshold << ": \n\t"; + for ( auto ord : vres ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + // Change operator to keep element less than threshold + op = "<"; + r = g->execute(); + REQUIRE( r ); + + std::cout << "Result after applying operator " << vop << " (from " << op + << " ) and threshold " << threshold << ": \n\t"; + for ( auto ord : vres ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + REQUIRE( *( std::max_element( vres.begin(), vres.end() ) ) < threshold ); + + // Change operator to keep element greater than threshold + op = ">"; + r = g->execute(); + REQUIRE( r ); + + std::cout << "Result after applying operator " << vop << " (from " << op + << " ) and threshold " << threshold << ": \n\t"; + for ( auto ord : vres ) { + std::cout << ord << ' '; + } + std::cout << '\n'; + REQUIRE( *( std::max_element( vres.begin(), vres.end() ) ) > threshold ); + } + SECTION( "Serialization of a custom graph" ) { + // Create and fill the factory for the custom nodes + auto customFactory = NodeFactoriesManager::createFactory( "CustomNodesUnitTests" ); + + // add node creators to the factory + bool registered; + registered = customFactory->registerNodeCreator( + Customs::CustomStringSource::getTypename() + "_", "Custom" ); + REQUIRE( registered == true ); + registered = customFactory->registerNodeCreator( + Customs::CustomStringSink::getTypename() + "_", "Custom" ); + REQUIRE( registered == true ); + registered = customFactory->registerNodeCreator>( + Customs::FilterSelector::getTypename() + "_", "Custom" ); + REQUIRE( registered == true ); + // The same node can't be register twice in the same factory + registered = customFactory->registerNodeCreator>( + Customs::FilterSelector::getTypename() + "_", "Custom" ); + REQUIRE( registered == false ); + + std::cout << "Building the following custom nodes with the factory " + << customFactory->getName() << "\n"; + for ( auto [name, functor] : customFactory->getFactoryMap() ) { + std::cout << name << ", "; + } + std::cout << "\n"; + + nlohmann::json emptyData; + auto customSource = customFactory->createNode( + Customs::CustomStringSource::getTypename(), emptyData, nullptr ); + REQUIRE( customSource != nullptr ); + + std::cout << "Created node " << customSource->getInstanceName() << " with type " + << customSource->getTypeName() << " // " + << Customs::CustomStringSource::getTypename() << "\n"; + + // build a graph + auto g = buildgraph( "testCustomNodes" ); + g->addFactory( customFactory ); + + std::string tmpdir { "customGraphExport/" }; + std::filesystem::create_directories( tmpdir ); + + // save the graph without factory + // save the graph with factory + g->saveToJson( tmpdir + "customGraph.json" ); + + g->destroy(); + delete g; + g = new DataflowGraph( "" ); + + bool loaded = g->loadFromJson( tmpdir + "customGraph.json" ); + + REQUIRE( loaded == true ); + g->destroy(); + delete g; + + /// try to load the graph without custom factory + auto unregistered = NodeFactoriesManager::unregisterFactory( customFactory->getName() ); + REQUIRE( unregistered == true ); + + g = new DataflowGraph( "" ); + loaded = g->loadFromJson( tmpdir + "customGraph.json" ); + REQUIRE( loaded == false ); + delete g; + + std::filesystem::remove_all( tmpdir ); + } +} diff --git a/tests/unittest/Dataflow/graph.cpp b/tests/unittest/Dataflow/graph.cpp new file mode 100644 index 00000000000..c6d32147583 --- /dev/null +++ b/tests/unittest/Dataflow/graph.cpp @@ -0,0 +1,423 @@ +#include + +#include + +#include +#include +#include + +using namespace Ra::Dataflow::Core; + +void inspectGraph( const DataflowGraph& g ) { + // Factories used by the graph + auto factories = g.getNodeFactories(); + std::cout << "Used factories by the graph \"" << g.getInstanceName() << "\" with type \"" + << g.getTypeName() << "\" :\n"; + for ( const auto& f : *( factories.get() ) ) { + std::cout << "\t" << f.first << "\n"; + } + + // Nodes of the graph + const auto& nodes = g.getNodes(); + std::cout << "Nodes of the graph " << g.getInstanceName() << " (" << nodes.size() << ") :\n"; + for ( const auto& n : nodes ) { + std::cout << "\t\"" << n->getInstanceName() << "\" of type \"" << n->getTypeName() + << "\"\n"; + // Inspect input, output and interfaces of the node + std::cout << "\t\tInput ports :\n"; + for ( const auto& p : n->getInputs() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() << "\n"; + } + std::cout << "\t\tOutput ports :\n"; + for ( const auto& p : n->getOutputs() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() << "\n"; + } + std::cout << "\t\tInterface ports :\n"; + for ( const auto& p : n->getInterfaces() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() << "\n"; + } + } + + // Nodes by level after the compilation + if ( g.isCompiled() ) { + auto& cn = g.getNodesByLevel(); + std::cout << "Nodes of the graph, sorted by level after compiling the graph :\n"; + for ( size_t i = 0; i < cn.size(); ++i ) { + std::cout << "\tLevel " << i << " :\n"; + for ( const auto n : cn[i] ) { + std::cout << "\t\t\"" << n->getInstanceName() << "\"\n"; + } + } + } + + // describe the graph interface : inputs and outputs port of the whole graph (not of the + // nodes) + std::cout << "Inputs and output ports of the graph " << g.getInstanceName() << " :\n"; + const auto& inputs = g.getInputs(); + std::cout << "\tInput ports (" << inputs.size() << ") are :\n"; + for ( const auto& inp : inputs ) { + std::cout << "\t\t\"" << inp->getName() << "\" accepting type \"" << inp->getTypeName() + << "\"\n"; + } + const auto& outputs = g.getOutputs(); + std::cout << "\tOutput ports (" << outputs.size() << ") are :\n"; + for ( const auto& outp : outputs ) { + std::cout << "\t\t\"" << outp->getName() << "\" accepting type \"" << outp->getTypeName() + << "\"\n"; + } + + std::cout << "DataSetters and DataGetters port of the graph " << g.getInstanceName() << " :\n"; + auto setters = g.getAllDataSetters(); + std::cout << "\tSetters ports (" << setters.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : setters ) { + std::cout << "\t\t\"" << portName << "\" accepting type \"" << portType << "\"\n"; + } + auto getters = g.getAllDataGetters(); + std::cout << "\tGetters ports (" << getters.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : getters ) { + std::cout << "\t\t\"" << portName << "\" generating type \"" << portType << "\"\n"; + } +} + +TEST_CASE( "Dataflow/Core/Graph", "[Dataflow][Core][Graph]" ) { + SECTION( "Creation of a graph" ) { + DataflowGraph g( "Test Graph" ); + // Test not a json error detection + auto result = g.loadFromJson( "data/Dataflow/NotAJsonFile.json" ); + REQUIRE( !result ); + // Test loading empty graph + nlohmann::json emptyJson = {}; + result = g.fromJson( emptyJson ); + REQUIRE( result ); + + // missing identification of the graph (either id or instance) + nlohmann::json noId = { { "model", { "name", "Core DataflowGraph" } } }; + result = g.fromJson( noId ); + REQUIRE( !result ); + g.destroy(); + + // missing model of the graph + nlohmann::json noModel = { { "instance", "No model in this node" } }; + result = g.fromJson( noModel ); + REQUIRE( !result ); + g.destroy(); + + // missing instance data --> loads an empty graph + nlohmann::json noGraph = { { "instance", "Missing instance data for model" }, + { "model", { "name", "Core DataflowGraph" } } }; + result = g.fromJson( noGraph ); + REQUIRE( result ); + g.destroy(); + + // Requesting unknown factory + nlohmann::json wrongFactory = { + { "instance", "unknown factory" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", { { "factories", { "NotAFactory" } } } } } } }; + result = g.fromJson( wrongFactory ); + REQUIRE( !result ); + g.destroy(); + + // trying to instance an unknown node type + nlohmann::json NotANode = { + { "instance", "graph with unknown node" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "NotANode" }, + { "model", { { "name", "NotANode" } } } } } } } } } } }; + result = g.fromJson( NotANode ); + REQUIRE( !result ); + g.destroy(); + + // trying to instance an unknown node type + nlohmann::json NoModelName = { + { "instance", "graph with missing node model information" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "Unknown model" }, + { "model", { { "extra", "NotaTypeName" } } } } } } } } } } }; + result = g.fromJson( NoModelName ); + REQUIRE( !result ); + g.destroy(); + + // trying to instance an unknown node type + nlohmann::json noInstanceIdentification = { + { "instance", "graph with missing node model information" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", { { { "model", { { "name", "Source" } } } } } } } } } } }; + result = g.fromJson( noInstanceIdentification ); + REQUIRE( !result ); + g.destroy(); + + // errors in the connection description + nlohmann::json reusingNodeIdentification = { + { "instance", "graph with wrong connection" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "Source" }, { "model", { { "name", "Source" } } } }, + { { "instance", "Source" }, { "model", { { "name", "Sink" } } } } } }, + { "connections", { { { "out_node", "wrongId" } } } } } } } } }; + result = g.fromJson( reusingNodeIdentification ); + REQUIRE( !result ); + g.destroy(); + reusingNodeIdentification = { + { "instance", "graph with wrong connection" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "model", { { "name", "Source" } } } }, + { { "model", { { "name", "Sink" } } } } } }, + { "connections", { { { "out_node", "wrongId" } } } } } } } } }; + result = g.fromJson( reusingNodeIdentification ); + REQUIRE( !result ); + g.destroy(); + + // errors in the connection description + nlohmann::json wrongConnection = { + { "instance", "graph with wrong connection" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "SourceFloat" }, + { "model", { { "name", "Source" } } } }, + { { "instance", "SinkInt" }, { "model", { { "name", "Sink" } } } } } }, + { "connections", { { { "out_node", "wrongId" } } } } } } } } }; + result = g.fromJson( wrongConnection ); + REQUIRE( !result ); + g.destroy(); + + wrongConnection = { + { "instance", "Test Graph Inline" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "SourceFloat" }, + { "model", { { "name", "Source" } } } }, + { { "instance", "SinkInt" }, { "model", { { "name", "Sink" } } } } } }, + { "connections", + { { { "out_node", "SourceFloat" }, { "out_index", 2 } } } } } } } } }; + result = g.fromJson( wrongConnection ); + REQUIRE( !result ); + g.destroy(); + + wrongConnection = { + { "instance", "Test Graph Inline" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "SourceFloat" }, + { "model", { { "name", "Source" } } } }, + { { "instance", "SinkInt" }, { "model", { { "name", "Sink" } } } } } }, + { "connections", + { { { "out_node", "SourceFloat" }, + { "out_index", 0 }, + { "in_node", "Sink" }, + { "in_port", "from" } } } } } } } } }; + result = g.fromJson( wrongConnection ); + REQUIRE( !result ); + g.destroy(); + + wrongConnection = { + { "instance", "Test Graph Inline" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "SourceFloat" }, + { "model", { { "name", "Source" } } } }, + { { "instance", "SinkInt" }, { "model", { { "name", "Sink" } } } } } }, + { "connections", + { { { "out_node", "SourceFloat" }, + { "out_index", 0 }, + { "in_node", "SinkInt" }, + { "in_port", "from" } } } } } } } } }; + result = g.fromJson( wrongConnection ); + REQUIRE( !result ); + g.destroy(); + + // Constructed a correct graph + nlohmann::json goodSimpleGraph = { + { "instance", "Test Graph Inline" }, + { "model", + { { "name", "Core DataflowGraph" }, + { "graph", + { { "factories", {} }, + { "nodes", + { { { "instance", "SourceFloat" }, + { "model", { { "name", "Source" } } } }, + { { "instance", "SinkFloat" }, + { "model", { { "name", "Sink" } } } } } }, + { "connections", + { { { "out_node", "SourceFloat" }, + { "out_port", "to" }, + { "in_node", "SinkFloat" }, + { "in_index", 0 } } } } } } } } }; + result = g.fromJson( goodSimpleGraph ); + REQUIRE( result ); + + // trying to add a duplicated node + auto duplicatedNodeName = new Sources::SingleDataSourceNode( "SourceFloat" ); + auto [r, rejectedNode] = g.addNode( std::unique_ptr( duplicatedNodeName ) ); + REQUIRE( !r ); + REQUIRE( rejectedNode == duplicatedNodeName ); + delete duplicatedNodeName; + + auto sinkFloatNode = g.getNode( "Sink" ); + REQUIRE( sinkFloatNode == nullptr ); + sinkFloatNode = g.getNode( "SinkFloat" ); + REQUIRE( sinkFloatNode != nullptr ); + auto sourceFloatNode = g.getNode( "SourceFloat" ); + REQUIRE( sourceFloatNode != nullptr ); + + auto sourceIntNode = new Sources::IntSource( "SourceInt" ); + auto sinkIntNode = new Sinks::IntSink( "SinkInt" ); + // node not found + result = g.removeLink( sinkIntNode, "from" ); + REQUIRE( !result ); + + // "from" node not found + result = g.addLink( sourceIntNode, "out", sinkIntNode, "in" ); + REQUIRE( !result ); + + auto resPair = g.addNode( std::unique_ptr( sourceIntNode ) ); + REQUIRE( resPair.first ); + // "to" node not found + result = g.addLink( sourceIntNode, "out", sinkIntNode, "in" ); + REQUIRE( !result ); + + resPair = g.addNode( std::unique_ptr( sinkIntNode ) ); + REQUIRE( resPair.first ); + // output port of "from" node not found + result = g.addLink( sourceIntNode, "out", sinkIntNode, "in" ); + REQUIRE( !result ); + + // input port of "to" node not found + result = g.addLink( sourceIntNode, "to", sinkIntNode, "in" ); + REQUIRE( !result ); + + // link OK + result = g.addLink( sourceIntNode, "to", sinkIntNode, "from" ); + REQUIRE( result ); + + // from por of "to" node already linked + result = g.addLink( sourceIntNode, "to", sinkIntNode, "from" ); + REQUIRE( !result ); + + // type mismatch + result = g.addLink( sourceIntNode, "to", sinkFloatNode, "from" ); + REQUIRE( !result ); + + // protect the graph to prevent link removal + g.setNodesAndLinksProtection( true ); + REQUIRE( g.getNodesAndLinksProtection() ); + // unable to remove links from protected graph ... + result = g.removeLink( sinkIntNode, "from" ); + REQUIRE( !result ); + g.setNodesAndLinksProtection( false ); + REQUIRE( !g.getNodesAndLinksProtection() ); + // remove link OK + result = g.removeLink( sinkIntNode, "from" ); + REQUIRE( result ); + + // input port not found to remove its link + result = g.removeLink( sinkIntNode, "in" ); + REQUIRE( !result ); + + // compile the graph + result = g.compile(); + REQUIRE( result ); + REQUIRE( g.isCompiled() ); + + // clear the graph + g.clearNodes(); + + // Nodes can't be found + auto nullNode = g.getNode( "SourceInt" ); + REQUIRE( nullNode == nullptr ); + nullNode = g.getNode( "SinkInt" ); + REQUIRE( nullNode == nullptr ); + // Nodes can't be found + nullNode = g.getNode( "SourceFloat" ); + REQUIRE( nullNode == nullptr ); + nullNode = g.getNode( "SinkFloat" ); + REQUIRE( nullNode == nullptr ); + + // destroy everything + g.destroy(); + } + + SECTION( "Inspection of a graph" ) { + std::cout << "Loading graph data/Dataflow/ExampleGraph.json\n"; + auto g = DataflowGraph::loadGraphFromJsonFile( "data/Dataflow/ExampleGraph.json" ); + + // Factories used by the graph + auto factories = g->getNodeFactories(); + REQUIRE( factories != nullptr ); + const auto& nodes = g->getNodes(); + REQUIRE( nodes.size() == g->getNodesCount() ); + auto c = g->compile(); + REQUIRE( c == true ); + REQUIRE( g->isCompiled() ); + // Prints the graph content + inspectGraph( *g ); + g->needsRecompile(); + REQUIRE( !g->isCompiled() ); + + // removing the boolean sink from the graph + auto n = g->getNode( "validation value" ); + REQUIRE( n->getInstanceName() == "validation value" ); + c = g->removeNode( n ); + REQUIRE( c == true ); + REQUIRE( n == nullptr ); + c = g->compile(); + REQUIRE( c == true ); + + // Simplified graph after compilation + auto& cn = g->getNodesByLevel(); + // the source "Validator" is no more in level 0 as it is not reachable from a sink in the + // graph. + auto found = std::find_if( cn[0].begin(), cn[0].end(), []( const auto& nn ) { + return nn->getInstanceName() == "Validator"; + } ); + REQUIRE( found == cn[0].end() ); + + // removing the source "Validator" + n = g->getNode( "Validator" ); + REQUIRE( n->getInstanceName() == "Validator" ); + // protect the graph to prevent node removal + g->setNodesAndLinksProtection( true ); + c = g->removeNode( n ); + REQUIRE( !c ); + g->setNodesAndLinksProtection( false ); + c = g->removeNode( n ); + REQUIRE( c ); + REQUIRE( n == nullptr ); + + std::cout << "####### Graph after sink and source removal\n"; + inspectGraph( *g ); + + delete g; + } +} diff --git a/tests/unittest/Dataflow/nodes.cpp b/tests/unittest/Dataflow/nodes.cpp new file mode 100644 index 00000000000..38aa33c694e --- /dev/null +++ b/tests/unittest/Dataflow/nodes.cpp @@ -0,0 +1,422 @@ +#include + +#include +#include + +#include + +#include + +#include +#include +#include + +//! [Create a source to sink graph for type T] +using namespace Ra::Dataflow::Core; +template +std::tuple, std::shared_ptr, PortBase*> +createGraph( + const std::string& name, + typename Functionals::BinaryOpNode::BinaryOperator f ) { + using TestNode = Functionals::BinaryOpNode; + auto g = new DataflowGraph { name }; + + auto source_a = new Sources::SingleDataSourceNode( "a" ); + g->addNode( std::unique_ptr( source_a ) ); + auto a = g->getDataSetter( "a_to" ); + REQUIRE( a->getNode() == g ); + + auto source_b = new Sources::SingleDataSourceNode( "b" ); + g->addNode( std::unique_ptr( source_b ) ); + auto b = g->getDataSetter( "b_to" ); + REQUIRE( b->getNode() == g ); + + auto sink = new Sinks::SinkNode( "r" ); + g->addNode( std::unique_ptr( sink ) ); + auto r = g->getDataGetter( "r_from" ); + REQUIRE( r->getNode() == g ); + + auto op = new TestNode( "operator", f ); + // op->setOperator( f ); + g->addNode( std::unique_ptr( op ) ); + + REQUIRE( g->addLink( source_a, "to", op, "a" ) ); + REQUIRE( g->addLink( op, "r", sink, "from" ) ); + REQUIRE( !g->compile() ); + // this will not execute the graph as it does not compile + g->execute(); + REQUIRE( !g->isCompiled() ); + // add missing link + REQUIRE( g->addLink( source_b, "to", op, "b" ) ); + + return { g, a, b, r }; +} + +TEST_CASE( "Dataflow/Core/Nodes", "[Dataflow][Core][Nodes]" ) { + SECTION( "Operations on Scalar" ) { + using DataType = Scalar; + using TestNode = Functionals::BinaryOpNode; + typename TestNode::BinaryOperator add = []( typename TestNode::Arg1_type a, + typename TestNode::Arg2_type b ) -> + typename TestNode::Res_type { return a + b; }; + + auto [g, a, b, r] = createGraph( "test scalar binary op", add ); + + DataType x { 1_ra }; + a->setData( &x ); + REQUIRE( a->getData() == x ); + + DataType y { 2_ra }; + b->setData( &y ); + REQUIRE( b->getData() == y ); + + // As graph was modified since last compilation, this will recompile the graph + g->execute(); + + auto& z = r->getData(); + REQUIRE( z == x + y ); + + std::cout << x << " + " << y << " == " << z << "\n"; + + g->destroy(); + delete g; + } + + SECTION( "Operations on Vectors" ) { + using DataType = Ra::Core::Vector3; + using TestNode = Functionals::BinaryOpNode; + typename TestNode::BinaryOperator add = []( typename TestNode::Arg1_type a, + typename TestNode::Arg2_type b ) -> + typename TestNode::Res_type { return a + b; }; + + auto [g, a, b, r] = createGraph( "test Vector3 binary op", add ); + + DataType x { 1_ra, 2_ra, 3_ra }; + a->setData( &x ); + REQUIRE( a->getData() == x ); + + DataType y { 3_ra, 2_ra, 1_ra }; + b->setData( &y ); + REQUIRE( b->getData() == y ); + + g->execute(); + + auto& z = r->getData(); + REQUIRE( z == x + y ); + + std::cout << "[" << x.transpose() << "] + [" << y.transpose() << "] == [" << z.transpose() + << "]\n"; + + g->destroy(); + delete g; + } + + SECTION( "Operations on VectorArrays" ) { + using DataType = Ra::Core::VectorArray; + using TestNode = Functionals::BinaryOpNode; + typename TestNode::BinaryOperator add = []( typename TestNode::Arg1_type a, + typename TestNode::Arg2_type b ) -> + typename TestNode::Res_type { return a + b; }; + + auto [g, a, b, r] = createGraph( "test Vector3 binary op", add ); + + DataType x { { 1_ra, 2_ra }, { 3_ra, 4_ra } }; + a->setData( &x ); + REQUIRE( a->getData() == x ); + + DataType y { { 5_ra, 6_ra }, { 7_ra, 8_ra } }; + b->setData( &y ); + REQUIRE( b->getData() == y ); + + g->execute(); + + auto& z = r->getData(); + for ( size_t i = 0; i < z.size(); i++ ) { + REQUIRE( z[i] == x[i] + y[i] ); + } + + std::cout << "{ "; + for ( const auto& t : x ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "} + { "; + for ( const auto& t : y ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "} = { "; + for ( const auto& t : z ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "}\n"; + + g->destroy(); + delete g; + } + + SECTION( "Operations between VectorArray and Scalar" ) { + using DataType_a = Ra::Core::VectorArray; + using DataType_b = Scalar; + // How to do this ? Eigen generates an error due to align allocation + // using DataType_r = Ra::Core::VectorArray< decltype( std::declval() * + // std::declval() ) >; + using DataType_r = Ra::Core::VectorArray; + using TestNode = Functionals::BinaryOpNode; + typename TestNode::BinaryOperator op = []( typename TestNode::Arg1_type a, + typename TestNode::Arg2_type b ) -> + typename TestNode::Res_type { return a * b; }; + auto [g, a, b, r] = createGraph( + "test Vector2 x Scalar binary op", op ); + + DataType_a x { { 1_ra, 2_ra }, { 3_ra, 4_ra } }; + a->setData( &x ); + REQUIRE( a->getData() == x ); + + DataType_b y { 5_ra }; + b->setData( &y ); + REQUIRE( b->getData() == y ); + + g->execute(); + + auto& z = r->getData(); + for ( size_t i = 0; i < z.size(); i++ ) { + REQUIRE( z[i] == x[i] * y ); + } + + std::cout << "{ "; + for ( const auto& t : x ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "} * " << y << " = { "; + for ( const auto& t : z ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "}\n"; + + // change operator + auto opNode = dynamic_cast( g->getNode( "operator" ) ); + REQUIRE( opNode != nullptr ); + if ( opNode ) { + typename TestNode::BinaryOperator f = []( typename TestNode::Arg1_type arg1, + typename TestNode::Arg2_type arg2 ) -> + typename TestNode::Res_type { return arg1 / arg2; }; + opNode->setOperator( f ); + } + g->execute(); + + for ( size_t i = 0; i < z.size(); i++ ) { + REQUIRE( z[i] == x[i] / y ); + } + + std::cout << "{ "; + for ( const auto& t : x ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "} / " << y << " = { "; + for ( const auto& t : z ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "}\n"; + g->destroy(); + delete g; + } + + SECTION( "Operations between Scalar and VectorArray" ) { + using namespace Ra::Dataflow::Core; + using DataType_a = Scalar; + using DataType_b = Ra::Core::VectorArray; + using DataType_r = Ra::Core::VectorArray; + using TestNode = Functionals::BinaryOpNode; + typename TestNode::BinaryOperator op = []( typename TestNode::Arg1_type a, + typename TestNode::Arg2_type b ) -> + typename TestNode::Res_type { return a * b; }; + auto [g, a, b, r] = createGraph( + "test Vector2 x Scalar binary op", op ); + + DataType_a x { 4_ra }; + a->setData( &x ); + REQUIRE( a->getData() == x ); + + DataType_b y { { 1_ra, 2_ra }, { 3_ra, 4_ra } }; + b->setData( &y ); + REQUIRE( b->getData() == y ); + + g->execute(); + + auto& z = r->getData(); + for ( size_t i = 0; i < z.size(); i++ ) { + REQUIRE( z[i] == x * y[i] ); + } + + std::cout << x << " * { "; + for ( const auto& t : y ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "} = { "; + for ( const auto& t : z ) { + std::cout << "[" << t.transpose() << "] "; + } + std::cout << "}\n"; + g->destroy(); + delete g; + } + + SECTION( "Transform/reduce/filter/test" ) { + //! [Create a complex transform/reduce graph] + auto g = new DataflowGraph( "Complex graph" ); + using VectorType = Ra::Core::VectorArray; + + // Source of a vector of Scalar : random vector + auto nodeS = new Sources::ScalarArraySource( "s" ); + + // Source of an operator on scalars : f(x) = 2*x + using DoubleFunction = Sources::FunctionSourceNode::function_type; + DoubleFunction doubleMe = []( const Scalar& x ) -> Scalar { return 2_ra * x; }; + auto nodeD = new Sources::FunctionSourceNode( "d" ); + nodeD->setData( &doubleMe ); + + // Source of a Scalar : mean neutral element 0_ra + auto nodeN = new Sources::ScalarSource( "n" ); + Scalar zero { 0_ra }; + nodeN->setData( &zero ); + + // Source of a reduction operator : compute the mean using Welford online algo + using ReduceOperator = Sources::FunctionSourceNode; + struct MeanOperator { + size_t n { 0 }; + Scalar operator()( const Scalar& m, const Scalar& x ) { + return m + ( ( x - m ) / ( ++n ) ); + } + }; + auto nodeM = new ReduceOperator( "m" ); + ReduceOperator::function_type m = MeanOperator(); + + // Reduce node : will compute the mean + using MeanCalculator = Functionals::ReduceNode; + auto meanCalculator = new MeanCalculator( "mean" ); + + // Sink for the mean + auto nodeR = new Sinks::ScalarSink( "r" ); + + // Transform operator, will double the vectors' values + auto nodeT = new Functionals::ArrayTransformerScalar( "twice" ); + + // Will compute the mean on the doubled vector + auto doubleMeanCalculator = new MeanCalculator( "double mean" ); + + // Sink for the double mean + auto nodeRD = new Sinks::ScalarSink( "rd" ); + + // Source for a comparison functor , eg f(x, y) -> 2*x == y + auto nodePred = new Sources::ScalarBinaryPredicateSource( "predicate" ); + Sources::ScalarBinaryPredicateSource::function_type predicate = + []( const Scalar& a, const Scalar& b ) -> bool { return 2_ra * a == b; }; + nodePred->setData( &predicate ); + + // Boolean sink for the validation result + auto sinkB = new Sinks::BooleanSink( "test" ); + + // Node for coparing the results of the computation graph + auto validator = new Functionals::BinaryOpNode( "validator" ); + + g->addNode( std::unique_ptr( nodeS ) ); + g->addNode( std::unique_ptr( nodeD ) ); + g->addNode( std::unique_ptr( nodeN ) ); + g->addNode( std::unique_ptr( nodeM ) ); + g->addNode( std::unique_ptr( nodeR ) ); + g->addNode( std::unique_ptr( meanCalculator ) ); + g->addNode( std::unique_ptr( doubleMeanCalculator ) ); + g->addNode( std::unique_ptr( nodeT ) ); + g->addNode( std::unique_ptr( nodeRD ) ); + + bool linkAdded; + linkAdded = g->addLink( nodeS, "to", meanCalculator, "in" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeM, "f", meanCalculator, "f" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeN, "to", meanCalculator, "init" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( meanCalculator, "out", nodeR, "from" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeS, "to", nodeT, "in" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeD, "f", nodeT, "f" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeT, "out", doubleMeanCalculator, "in" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( doubleMeanCalculator, "out", nodeRD, "from" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodeM, "f", doubleMeanCalculator, "f" ); + REQUIRE( linkAdded == true ); + + g->addNode( std::unique_ptr( nodePred ) ); + g->addNode( std::unique_ptr( sinkB ) ); + g->addNode( std::unique_ptr( validator ) ); + linkAdded = g->addLink( meanCalculator, "out", validator, "a" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( doubleMeanCalculator, "out", validator, "b" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( nodePred, "f", validator, "f" ); + REQUIRE( linkAdded == true ); + linkAdded = g->addLink( validator, "r", sinkB, "from" ); + REQUIRE( linkAdded == true ); + + auto input = g->getDataSetter( "s_to" ); + auto output = g->getDataGetter( "r_from" ); + auto outputD = g->getDataGetter( "rd_from" ); + auto outputB = g->getDataGetter( "test_from" ); + auto inputR = g->getDataSetter( "m_f" ); + if ( inputR == nullptr ) { std::cout << "Failed to get the graph function input !!\n"; } + + // Inspect the graph interface : inputs and outputs port + auto inputs = g->getAllDataSetters(); + std::cout << "Input ports (" << inputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : inputs ) { + std::cout << "\t\"" << portName << "\" accepting type " << portType << "\n"; + } + auto outputs = g->getAllDataGetters(); + std::cout << "Output ports (" << outputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : outputs ) { + std::cout << "\t\"" << portName << "\" generating type " << portType << "\n"; + } + + if ( !g->compile() ) { std::cout << "Compilation error !!"; } + + // Set input/ouput data + VectorType test; + input->setData( &test ); + + test.reserve( 10 ); + std::mt19937 gen( 0 ); + std::uniform_real_distribution<> dis( 0.0, 1.0 ); + // Fill the vector with random numbers between 0 and 1 + for ( size_t n = 0; n < test.capacity(); ++n ) { + test.push_back( dis( gen ) ); + } + + // No need to do this as mean operator source has a copy of a functor + ReduceOperator::function_type m1 = MeanOperator(); + inputR->setData( &m1 ); + + g->execute(); + + auto& result = output->getData(); + auto& resultD = outputD->getData(); + auto& resultB = outputB->getData(); + + std::cout << "Computed mean ( ref ): " << result << "\n"; + std::cout << "Computed mean ( tra ): " << resultD << "\n"; + std::cout << std::boolalpha; + std::cout << "Ratio ( expected 2 ): " << resultD / result << " -- validator --> " + << resultB << "\n"; + + std::cout << '\n'; + + REQUIRE( resultD / result == 2_ra ); + REQUIRE( resultB ); + // uncomment this if you want to edit the generated graph with GraphEditor + // g->saveToJson( "Transform-reduce.json" ); + g->destroy(); + delete g; + //! [Create a complex transform/reduce graph] + } +} diff --git a/tests/unittest/Dataflow/renderinggraph.cpp b/tests/unittest/Dataflow/renderinggraph.cpp new file mode 100644 index 00000000000..60f18375036 --- /dev/null +++ b/tests/unittest/Dataflow/renderinggraph.cpp @@ -0,0 +1,91 @@ +#include + +#include + +#include + +using namespace Ra::Dataflow::Core; +using namespace Ra::Dataflow::Rendering; + +TEST_CASE( "Dataflow/Rendering/RenderingGraph", "[Dataflow][Rendering][RenderingGraph]" ) { +#if defined( OS_LINUX ) + { + // This is required on Linux to force loading the libDataflowRendering.so so that the + // node factory for Rendering is initialized. + // If not present, as no symbols are explicitly used from this lib, the linker + // optimize out the lib. + // All the test below use only the general interface of a DataflowGraph, the dependency to + // symbols exported by the rendering lib is only managed by the node factory. + RenderingGraph gr( "Forcing libDataflowRendering.so to be loaded" ); + } +#endif + SECTION( "Loads and inspect a rendering graph graph" ) { + auto g = DataflowGraph::loadGraphFromJsonFile( "data/Dataflow/fullRenderingGraph.json" ); + + // Factories used by the graph + auto factories = g->getNodeFactories(); + REQUIRE( factories != nullptr ); + std::cout << "Used factories by the graph \"" << g->getInstanceName() << "\" with type \"" + << g->getTypeName() << "\" :\n"; + for ( const auto& f : *( factories.get() ) ) { + std::cout << "\t" << f.first << "\n"; + } + + // Nodes of the graph + const auto& nodes = g->getNodes(); + REQUIRE( nodes.size() == g->getNodesCount() ); + std::cout << "Nodes of the graph " << g->getInstanceName() << " (" << nodes.size() + << " nodes) " << g->getNodesCount() << ":\n"; + + for ( const auto& n : nodes ) { + std::cout << "\t\"" << n->getInstanceName() << "\" of type \"" << n->getTypeName() + << "\"\n"; + // Inspect input, output and interfaces of the node + std::cout << "\t\tInput ports :\n"; + for ( const auto& p : n->getInputs() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() + << "\n"; + } + std::cout << "\t\tOutput ports :\n"; + for ( const auto& p : n->getOutputs() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() + << "\n"; + } + std::cout << "\t\tInterface ports :\n"; + for ( const auto& p : n->getInterfaces() ) { + std::cout << "\t\t\t\"" << p->getName() << "\" with type " << p->getTypeName() + << "\n"; + } + } + + // describe the graph interface : inputs and outputs port of the whole graph (not of the + // nodes) + std::cout << "Inputs and output nodes of the graph " << g->getInstanceName() << " :\n"; + auto inputs = g->getAllDataSetters(); + std::cout << "\tInput ports (" << inputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : inputs ) { + std::cout << "\t\t\"" << portName << "\" accepting type \"" << portType << "\"\n"; + } + auto outputs = g->getAllDataGetters(); + std::cout << "\tOutput ports (" << outputs.size() << ") are :\n"; + for ( auto& [ptrPort, portName, portType] : outputs ) { + std::cout << "\t\t\"" << portName << "\" generating type \"" << portType << "\"\n"; + } + +// Can't compile the graph without OpenGL context (it's a rendering graph, compilation needs openGL +// context active) +#if 0 + // Nodes by level after the compilation + auto c = g->compile(); + REQUIRE( c == true ); + auto& cn = g->getNodesByLevel(); + std::cout << "Nodes of the graph, sorted by level when compiling the graph :\n"; + for ( size_t i = 0; i < cn.size(); ++i ) { + std::cout << "\tLevel " << i << " :\n"; + for ( const auto n : cn[i] ) { + std::cout << "\t\t\"" << n->getInstanceName() << "\"\n"; + } + } +#endif + } +} diff --git a/tests/unittest/Dataflow/serialization.cpp b/tests/unittest/Dataflow/serialization.cpp new file mode 100644 index 00000000000..21d8e180afb --- /dev/null +++ b/tests/unittest/Dataflow/serialization.cpp @@ -0,0 +1,106 @@ +#include + +#include +#include + +#include + +#include + +#include +#include +#include +#include + +TEST_CASE( "Dataflow/Core/DataflowGraph", "[Dataflow][Core][DataflowGraph]" ) { + SECTION( "Execution and modification of a graph" ) { + using namespace Ra::Dataflow::Core; + using DataType = Scalar; + DataflowGraph g { "original graph" }; + g.addJsonMetaData( + { { "extra", { { "info", "missing operators on functional node" } } } } ); + auto source_a = new Sources::SingleDataSourceNode( "a" ); + g.addNode( std::unique_ptr( source_a ) ); + auto a = g.getDataSetter( "a_to" ); + auto source_b = new Sources::SingleDataSourceNode( "b" ); + g.addNode( std::unique_ptr( source_b ) ); + auto b = g.getDataSetter( "b_to" ); + auto sink = new Sinks::SinkNode( "r" ); + g.addNode( std::unique_ptr( sink ) ); + auto r = g.getDataGetter( "r_from" ); + using TestNode = Functionals::BinaryOpNode; + TestNode::BinaryOperator add = []( TestNode::Arg1_type a, + TestNode::Arg2_type b ) -> TestNode::Res_type { + return a + b; + }; + auto op_unique = std::make_unique( "addition" ); + op_unique->setOperator( add ); + auto [added, op] = g.addNode( std::move( op_unique ) ); + REQUIRE( added ); + g.addLink( source_a, "to", op, "a" ); + g.addLink( op, "r", sink, "from" ); + g.addLink( source_b, "to", op, "b" ); + + // execution of the original graph + DataType x { 1_ra }; + a->setData( &x ); + DataType y { 2_ra }; + b->setData( &y ); + // Execute initial graph"; + g.execute(); + auto z = r->getData(); + REQUIRE( z == x + y ); + + // Save the graph + std::string tmpdir { "tmpDir4Tests" }; + std::filesystem::create_directories( tmpdir ); + g.saveToJson( tmpdir + "/GraphSerializationTest.json" ); + g.destroy(); + // this does nothing as g was destroyed + g.execute(); + + // Create a new graph and load from the saved graph + DataflowGraph g1 { "loaded graph" }; + g1.loadFromJson( tmpdir + "/GraphSerializationTest.json" ); + + // Setting the unserializable data on nodes (functions) + auto addition = g1.getNode( "addition" ); + REQUIRE( addition != nullptr ); + REQUIRE( addition->getTypeName() == Functionals::BinaryOpScalar::getTypename() ); + auto typedAddition = dynamic_cast( addition ); + REQUIRE( typedAddition != nullptr ); + if ( typedAddition != nullptr ) { typedAddition->setOperator( add ); } + + // Execute loaded graph + // Data delivered by the source nodes are the one saved by the original graph + g1.execute(); + auto r_loaded = g1.getDataGetter( "r_from" ); + auto& z_loaded = r_loaded->getData(); + REQUIRE( z_loaded == z ); + + auto a_loaded = g1.getDataSetter( "a_to" ); + auto b_loaded = g1.getDataSetter( "b_to" ); + DataType xp { 2_ra }; + a_loaded->setData( &xp ); + DataType yp { 3_ra }; + b_loaded->setData( &yp ); + g1.execute(); + REQUIRE( z_loaded == 5 ); + + // Reset sources to use the loaded data loaded + g1.releaseDataSetter( "a_to" ); + g1.releaseDataSetter( "b_to" ); + g1.execute(); + REQUIRE( z_loaded == z ); + + // reactivate the dataSetter and change the data delivered by a (copy data into a) + g1.activateDataSetter( "b_to" ); + auto loadedSource_a = + dynamic_cast*>( g1.getNode( "a" ) ); + Scalar newX = 3_ra; + loadedSource_a->setData( &newX ); + g1.execute(); + REQUIRE( z_loaded == 6 ); + std::filesystem::remove_all( tmpdir ); + } +} diff --git a/tests/unittest/Dataflow/sourcesandsinks.cpp b/tests/unittest/Dataflow/sourcesandsinks.cpp new file mode 100644 index 00000000000..7992c705409 --- /dev/null +++ b/tests/unittest/Dataflow/sourcesandsinks.cpp @@ -0,0 +1,161 @@ +#include + +#include +#include + +#include + +#include +#include +#include + +using namespace Ra::Dataflow::Core; + +//! [Create a source to sink graph for type T] +template +void testGraph( const std::string& name, T in, T& out ) { + auto g = new DataflowGraph { name }; + auto source = new Sources::SingleDataSourceNode( "in" ); + auto sink = new Sinks::SinkNode( "out" ); + g->addNode( std::unique_ptr( source ) ); + g->addNode( std::unique_ptr( sink ) ); + auto linked = g->addLink( source, "to", sink, "from" ); + if ( !linked ) { std::cerr << "Error linking source and sink nodes.\n"; } + REQUIRE( linked ); + + auto input = g->getDataSetter( "in_to" ); + REQUIRE( input != nullptr ); + auto output = g->getDataGetter( "out_from" ); + REQUIRE( output != nullptr ); + + auto compiled = g->compile(); + if ( !compiled ) { std::cerr << "Error compiling graph.\n"; } + REQUIRE( compiled ); + + std::cout << "Setting " << simplifiedDemangledType() << " data on interface port ... "; + input->setData( &in ); + + g->execute(); + + T r = output->getData(); + std::cout << "Getting a " << simplifiedDemangledType( r ) << " from interface port ... "; + out = r; + + g->releaseDataSetter( "in_to" ); + source->setData( &in ); + + nlohmann::json graphData; + g->toJson( graphData ); + g->destroy(); + delete g; + + g = new DataflowGraph { name }; + g->fromJson( graphData ); + auto ok = g->execute(); + REQUIRE( ok ); + delete g; +} +//! [Create a source to sink graph for type T] +TEST_CASE( "Dataflow/Core/Sources and Sinks", "[Dataflow][Core][Sources and Sinks]" ) { + SECTION( "Operations on base type : Scalar" ) { + using DataType = Scalar; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + + DataType x { 3.141592_ra }; + DataType y { 0_ra }; + testGraph( "Test on Scalar", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : float" ) { + using DataType = float; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 3.141592 }; + DataType y { 0 }; + testGraph( "Test on float", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : double" ) { + using DataType = double; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 3.141592 }; + DataType y { 0 }; + testGraph( "Test on float", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : int" ) { + using DataType = int; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { -3 }; + DataType y { 0 }; + testGraph( "Test on int", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : unsigned int" ) { + using DataType = unsigned int; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 3 }; + DataType y { 0 }; + testGraph( "Test on unsigned int", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : bool" ) { + using DataType = bool; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { true }; + DataType y { false }; + testGraph( "Test on bool", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : Vector2" ) { + using DataType = Ra::Core::Vector2; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 1_ra, 2_ra }; + DataType y; + testGraph( "Test on Vector2", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : Vector3" ) { + using DataType = Ra::Core::Vector3; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 1_ra, 2_ra, 3_ra }; + DataType y; + testGraph( "Test on Vector3", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : Vector4" ) { + using DataType = Ra::Core::Vector4; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x { 1_ra, 2_ra, 3_ra, 4_ra }; + DataType y; + testGraph( "Test on Vector4", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } + SECTION( "Operations on base type : Color" ) { + using DataType = Ra::Core::Utils::Color; + std::cout << "Test on " << simplifiedDemangledType() << " ... "; + DataType x = Ra::Core::Utils::Color::Skin(); + DataType y; + testGraph( "Test on Color", x, y ); + + REQUIRE( x == y ); + std::cout << " ... DONE!\n"; + } +} diff --git a/tests/unittest/data/Dataflow/ExampleGraph.json b/tests/unittest/data/Dataflow/ExampleGraph.json new file mode 100644 index 00000000000..96025e0cb30 --- /dev/null +++ b/tests/unittest/data/Dataflow/ExampleGraph.json @@ -0,0 +1,220 @@ +{ + "instance": "Example graph", + "model": { + "graph": { + "connections": [ + { + "in_node": "reduced vector", + "in_port": "from", + "out_node": "Collection reducer - original collection", + "out_port": "out" + }, + { + "in_node": "Collection reducer - original collection", + "in_port": "in", + "out_node": "Vector", + "out_port": "to" + }, + { + "in_node": "Collection reducer - original collection", + "in_port": "f", + "out_node": "Reducer", + "out_port": "f" + }, + { + "in_node": "Collection reducer - original collection", + "in_port": "init", + "out_node": "Neutral", + "out_port": "to" + }, + { + "in_node": "Collection reducer - transformed collection", + "in_port": "in", + "out_node": "Collection transformer", + "out_port": "out" + }, + { + "in_node": "Collection reducer - transformed collection", + "in_port": "f", + "out_node": "Reducer", + "out_port": "f" + }, + { + "in_node": "Collection transformer", + "in_port": "in", + "out_node": "Vector", + "out_port": "to" + }, + { + "in_node": "Collection transformer", + "in_port": "f", + "out_node": "Doubler", + "out_port": "f" + }, + { + "in_node": "transformed/reduced vector", + "in_port": "from", + "out_node": "Collection reducer - transformed collection", + "out_port": "out" + }, + { + "in_node": "validation value", + "in_port": "from", + "out_node": "Validator : evaluate the validation predicate", + "out_port": "r" + }, + { + "in_node": "Validator : evaluate the validation predicate", + "in_port": "a", + "out_node": "Collection reducer - original collection", + "out_port": "out" + }, + { + "in_node": "Validator : evaluate the validation predicate", + "in_port": "b", + "out_node": "Collection reducer - transformed collection", + "out_port": "out" + }, + { + "in_node": "Validator : evaluate the validation predicate", + "in_port": "f", + "out_node": "Validator", + "out_port": "f" + } + ], + "factories": [], + "nodes": [ + { + "instance": "Vector", + "model": { + "comment": "Unable to save data when serializing a SingleDataSourceNode>.", + "name": "Source>" + }, + "position": { + "x": -288.0, + "y": 58.0 + } + }, + { + "instance": "Doubler", + "model": { + "comment": "Unable to save data when serializing a FunctionSourceNode>.", + "name": "Source>" + }, + "position": { + "x": -290.0, + "y": 139.0 + } + }, + { + "instance": "Neutral", + "model": { + "name": "Source", + "number": 0.0 + }, + "position": { + "x": -289.0, + "y": -83.0 + } + }, + { + "instance": "Reducer", + "model": { + "comment": "Unable to save data when serializing a FunctionSourceNode>.", + "name": "Source>" + }, + "position": { + "x": -292.0, + "y": 224.0 + } + }, + { + "instance": "reduced vector", + "model": { + "name": "Sink" + }, + "position": { + "x": 459.0, + "y": -140.0 + } + }, + { + "instance": "Collection reducer - original collection", + "model": { + "comment": "Reduce operator could not be serialized for Reduce>", + "name": "Reduce>" + }, + "position": { + "x": 159.0, + "y": -133.0 + } + }, + { + "instance": "Collection reducer - transformed collection", + "model": { + "comment": "Reduce operator could not be serialized for Reduce>", + "name": "Reduce>" + }, + "position": { + "x": 461.0, + "y": 79.0 + } + }, + { + "instance": "Collection transformer", + "model": { + "comment": "Transform operator could not be serialized for Transform>", + "name": "Transform>" + }, + "position": { + "x": 159.0, + "y": 53.0 + } + }, + { + "instance": "transformed/reduced vector", + "model": { + "name": "Sink" + }, + "position": { + "x": 720.0, + "y": -62.0 + } + }, + { + "instance": "Validator", + "model": { + "comment": "Unable to save data when serializing a FunctionSourceNode>.", + "name": "Source>" + }, + "position": { + "x": -291.0, + "y": 309.0 + } + }, + { + "instance": "validation value", + "model": { + "name": "Sink" + }, + "position": { + "x": 969.0, + "y": 197.0 + } + }, + { + "instance": "Validator : evaluate the validation predicate", + "model": { + "comment": "Binary operator could not be serialized for BinaryOp bool>", + "name": "BinaryOp bool>" + }, + "position": { + "x": 705.0, + "y": 158.0 + } + } + ] + }, + "name": "Core DataflowGraph" + } +} diff --git a/tests/unittest/data/Dataflow/NotAJsonFile.json b/tests/unittest/data/Dataflow/NotAJsonFile.json new file mode 100644 index 00000000000..4fc8f92e8c1 --- /dev/null +++ b/tests/unittest/data/Dataflow/NotAJsonFile.json @@ -0,0 +1 @@ +# This file is not a valid json file diff --git a/tests/unittest/data/Dataflow/fullRenderingGraph.json b/tests/unittest/data/Dataflow/fullRenderingGraph.json new file mode 100644 index 00000000000..df5d47e8c63 --- /dev/null +++ b/tests/unittest/data/Dataflow/fullRenderingGraph.json @@ -0,0 +1,465 @@ +{ + "instance": "fullRenderingGraph", + "model": { + "graph": { + "connections": [ + { + "in_node": "Display Node", + "in_port": "Beauty", + "out_node": "wireframe_14", + "out_port": "Beauty" + }, + { + "in_node": "Display Node", + "in_port": "AOV_0", + "out_node": "geometryAovs_6", + "out_port": "world normal" + }, + { + "in_node": "Display Node", + "in_port": "AOV_1", + "out_node": "geometryAovs_6", + "out_port": "world pos" + }, + { + "in_node": "Display Node", + "in_port": "AOV_2", + "out_node": "transparency_11", + "out_port": "revealage" + }, + { + "in_node": "Display Node", + "in_port": "AOV_3", + "out_node": "transparency_11", + "out_port": "accum" + }, + { + "in_node": "Display Node", + "in_port": "AOV_4", + "out_node": "ssao_12", + "out_port": "ssao" + }, + { + "in_node": "clearFrame_9", + "in_port": "texture", + "out_node": "colorTex_8", + "out_port": "texture" + }, + { + "in_node": "clearFrame_9", + "in_port": "clear color", + "out_node": "Source_1", + "out_port": "to" + }, + { + "in_node": "clearFrame_9", + "in_port": "environment", + "out_node": "envmap_10", + "out_port": "to" + }, + { + "in_node": "clearFrame_9", + "in_port": "camera", + "out_node": "scene_6", + "out_port": "camera" + }, + { + "in_node": "geometryAovs_6", + "in_port": "objects", + "out_node": "scene_6", + "out_port": "objects" + }, + { + "in_node": "geometryAovs_6", + "in_port": "camera", + "out_node": "scene_6", + "out_port": "camera" + }, + { + "in_node": "emissivity_7", + "in_port": "objects", + "out_node": "scene_6", + "out_port": "objects" + }, + { + "in_node": "emissivity_7", + "in_port": "camera", + "out_node": "scene_6", + "out_port": "camera" + }, + { + "in_node": "emissivity_7", + "in_port": "color", + "out_node": "clearFrame_9", + "out_port": "Beauty" + }, + { + "in_node": "emissivity_7", + "in_port": "depth", + "out_node": "geometryAovs_6", + "out_port": "depth" + }, + { + "in_node": "emissivity_7", + "in_port": "AO", + "out_node": "ssao_12", + "out_port": "ssao" + }, + { + "in_node": "ssao_12", + "in_port": "worldPosition", + "out_node": "geometryAovs_6", + "out_port": "world pos" + }, + { + "in_node": "ssao_12", + "in_port": "worldNormal", + "out_node": "geometryAovs_6", + "out_port": "world normal" + }, + { + "in_node": "ssao_12", + "in_port": "camera", + "out_node": "scene_6", + "out_port": "camera" + }, + { + "in_node": "envlight_8", + "in_port": "objects", + "out_node": "scene_6", + "out_port": "objects" + }, + { + "in_node": "envlight_8", + "in_port": "camera", + "out_node": "scene_6", + "out_port": "camera" + }, + { + "in_node": "envlight_8", + "in_port": "envmap", + "out_node": "envmap_10", + "out_port": "to" + }, + { + "in_node": "envlight_8", + "in_port": "color", + "out_node": "emissivity_7", + "out_port": "Beauty" + }, + { + "in_node": "envlight_8", + "in_port": "depth", + "out_node": "geometryAovs_6", + "out_port": "depth" + }, + { + "in_node": "localLight_9", + "in_port": "objects", + "out_node": "scene_10", + "out_port": "objects" + }, + { + "in_node": "localLight_9", + "in_port": "lights", + "out_node": "scene_10", + "out_port": "lights" + }, + { + "in_node": "localLight_9", + "in_port": "camera", + "out_node": "scene_10", + "out_port": "camera" + }, + { + "in_node": "localLight_9", + "in_port": "color", + "out_node": "envlight_8", + "out_port": "Beauty" + }, + { + "in_node": "localLight_9", + "in_port": "depth", + "out_node": "geometryAovs_6", + "out_port": "depth" + }, + { + "in_node": "localLight_9", + "in_port": "AO", + "out_node": "ssao_12", + "out_port": "ssao" + }, + { + "in_node": "transparency_11", + "in_port": "objects", + "out_node": "scene_10", + "out_port": "objects" + }, + { + "in_node": "transparency_11", + "in_port": "lights", + "out_node": "scene_10", + "out_port": "lights" + }, + { + "in_node": "transparency_11", + "in_port": "camera", + "out_node": "scene_10", + "out_port": "camera" + }, + { + "in_node": "transparency_11", + "in_port": "color", + "out_node": "localLight_9", + "out_port": "Beauty" + }, + { + "in_node": "transparency_11", + "in_port": "depth", + "out_node": "geometryAovs_6", + "out_port": "depth" + }, + { + "in_node": "volumeLighting_13", + "in_port": "objects", + "out_node": "scene_10", + "out_port": "objects" + }, + { + "in_node": "volumeLighting_13", + "in_port": "lights", + "out_node": "scene_10", + "out_port": "lights" + }, + { + "in_node": "volumeLighting_13", + "in_port": "camera", + "out_node": "scene_10", + "out_port": "camera" + }, + { + "in_node": "volumeLighting_13", + "in_port": "color", + "out_node": "transparency_11", + "out_port": "Beauty" + }, + { + "in_node": "volumeLighting_13", + "in_port": "depth", + "out_node": "geometryAovs_6", + "out_port": "depth" + }, + { + "in_node": "wireframe_14", + "in_port": "objects", + "out_node": "scene_10", + "out_port": "objects" + }, + { + "in_node": "wireframe_14", + "in_port": "camera", + "out_node": "scene_10", + "out_port": "camera" + }, + { + "in_node": "wireframe_14", + "in_port": "color", + "out_node": "volumeLighting_13", + "out_port": "Beauty" + }, + { + "in_node": "wireframe_14", + "in_port": "depth", + "out_node": "geometryAovs_6", + "out_port": "depth" + }, + { + "in_node": "wireframe_14", + "in_port": "activate", + "out_node": "Source_2", + "out_port": "to" + } + ], + "factories": [ + "RenderingNodes" + ], + "nodes": [ + { + "instance": "scene_6", + "model": { + "name": "Scene data provider" + }, + "position": { + "x": 247.2943878173828, + "y": 323.0783996582031 + } + }, + { + "instance": "Source_1", + "model": { + "color": [ + 1.7104668586398475e-05, + 1.7104668586398475e-05, + 1.7104668586398475e-05, + 1.0 + ], + "name": "Source" + }, + "position": { + "x": 232.92669677734375, + "y": 11.072956085205078 + } + }, + { + "instance": "scene_10", + "model": { + "name": "Scene data provider" + }, + "position": { + "x": 1360.0, + "y": 829.5999755859375 + } + }, + { + "instance": "Source_2", + "model": { + "boolean": false, + "name": "Source" + }, + "position": { + "x": 2300.37158203125, + "y": 933.0 + } + }, + { + "instance": "Display Node", + "model": { + "name": "Display Sink" + }, + "position": { + "x": 2938.12109375, + "y": 690.5317993164063 + } + }, + { + "instance": "colorTex_8", + "model": { + "name": "Color Texture" + }, + "position": { + "x": 350.04608154296875, + "y": -71.95873260498047 + } + }, + { + "instance": "clearFrame_9", + "model": { + "clearColor": [ + 0.45098042488098145, + 0.45098042488098145, + 0.45098042488098145 + ], + "name": "Clear Color Pass" + }, + "position": { + "x": 541.1845703125, + "y": 60.45983123779297 + } + }, + { + "instance": "envmap_10", + "model": { + "name": "Source" + }, + "position": { + "x": 51.3619384765625, + "y": 477.6835021972656 + } + }, + { + "instance": "geometryAovs_6", + "model": { + "name": "Geometry AOVs" + }, + "position": { + "x": 639.5306396484375, + "y": 562.627197265625 + } + }, + { + "instance": "emissivity_7", + "model": { + "name": "Emissivity Render Node" + }, + "position": { + "x": 1031.2784423828125, + "y": 155.7407989501953 + } + }, + { + "instance": "ssao_12", + "model": { + "name": "SSAO Node", + "radius": 0.0 + }, + "position": { + "x": 802.4400024414063, + "y": 758.280029296875 + } + }, + { + "instance": "envlight_8", + "model": { + "name": "Env Lighting Node" + }, + "position": { + "x": 1358.296630859375, + "y": 230.74176025390625 + } + }, + { + "instance": "localLight_9", + "model": { + "name": "Local Lighting Node" + }, + "position": { + "x": 1661.003173828125, + "y": 308.92669677734375 + } + }, + { + "instance": "transparency_11", + "model": { + "name": "Transparency Local Lighting Node" + }, + "position": { + "x": 1962.969970703125, + "y": 383.0400085449219 + } + }, + { + "instance": "volumeLighting_13", + "model": { + "name": "Volume Local Lighting Node" + }, + "position": { + "x": 2292.723388671875, + "y": 455.3833312988281 + } + }, + { + "instance": "wireframe_14", + "model": { + "activated": false, + "name": "Wireframe Node" + }, + "position": { + "x": 2602.789794921875, + "y": 590.2335815429688 + } + } + ] + }, + "name": "Rendering Graph" + } +}