Processor Examples
In this section we want to introduce a few simple processor examples which should help developers during their first implementations. All examples can be found in the current Voreen release within the module "SampleModule".
Sample Processor
The SampleProcessor is a spartan version of a Voreen processor. It just implements all pure virtual functions inherited from the base classes. The processor can be tested in the workspace "SampleProcessor_workspace.vws" in the SampleModule. The image above is showing this workspace. In the workspace a TextSource provides the text "Hello World!". The SampleProcessor simply adds the prefix "Simon says:" to its front. The other three processors (Background, TextOverlay and Canvas) only produce a visible output. The Canvas shows the original text on the top and the modified text on the bottom. The user can change the orignial text and prefix by modifying the StringProperties on the right-hand side. We will now have a closer look into the implementation details.
sampleprocessor.h
#ifndef VRN_SAMPLEPROCESSOR_H
#define VRN_SAMPLEPROCESSOR_H
//add base class header
#include "voreen/core/processors/processor.h"
//add used port headers
#include "voreen/core/ports/textport.h"
//add used property headers
#include "voreen/core/properties/stringproperty.h"
//use namespace voreen
namespace voreen {
|
The first part of the header is simple basic c++. We check, if the processor has been already defined and add all needed headers. It should be mentioned, that all Voreen processors are defined and implemented in the voreen namespace.
/**
* Sample processor, which adds a user-defined prefix to a given text.
* VRN_CORE_API is a macro needed for shared libs on windows (see voreencoreapi.h)
*/
class VRN_CORE_API SampleProcessor : public Processor {
public:
/**
* Constructor
*/
SampleProcessor();
|
After all includes have been set we can start the class definition. All Voreen processors should inherit from the processor base class. The VRN_CORE_API macro is needed for DLL inports and exports under windows. This macro has no effect under Linux.
The constructor of a processor should have no parameter! All parameters the processor needs should be handed to it via ports and properties.
//------------------------------------------
// Pure virtual functions of base classes
//------------------------------------------
/**
* Virtual constructor
* @see VoreenSerializableObject
*/
virtual Processor* create() const
/**
* Function to get the class name, which is not
* directly supported by c++.
* @see VoreenSerializableObject
*/
virtual std::string getClassName() const;
/**
* Function to return the catagory of the processor.
* It will be shown in the VoreenVE GUI.
* @see Processor
*/
virtual std::string getCategory() const;
protected:
/**
* Function to set a description of the processors functionality.
* It will be shown in the VoreenVE GUI.
* @see Processor
*/
virtual void setDescriptions();
/**
* The main function of each processor.
* The main functionality should be implemented here.
* @see Processor
*/
virtual void process();
|
Here we define all pure virtual functions of the base classes. More information on this functions were presented in the section Pure Virtual Functions.
private:
//-------------
// members
//-------------
TextPort inport_; ///< inport used to receive the text to be modified
TextPort outport_; ///< outport used to pass the modified text
StringProperty prefixProp_; ///< property for the user-defined prefix
};
} // namespace
#endif // VRN_SAMPLEPROCESSOR_H
|
Last but not least we have to add the class members to our processor. In our case they are two ports for the in- and output of the text and a property to modify the prefix by the user.
sampleprocessor.cpp
1
2
3
4
5
|
//include header file
#include "sampleprocessor.h"
//using namespace voreen
namespace voreen {
|
Like in sampleprocessor.h only basic c++ at the beginning of the cpp file.
SampleProcessor::SampleProcessor()
//constructor of base class
: Processor()
//constructors of ports
, inport_(Port::INPORT, ///< port type, i.e inport
"inport", ///< unique port ID (unique for each processor)
"Unmodified Text Inport") ///< port name used in the VoreenVE GUI
, outport_(Port::OUTPORT, ///< port type, i.e outport
"outport", ///< unique port ID (unique for each processor)
"Modified Text Outport" ///< port name used in the VoreenVE GUI
//constructor of property
, prefixProp_("prefixProp", ///< unique property ID (unique for each processor)
"Prefix", ///< property name used in the VoreenVE GUI
"Simon says: ") ///< default value of the property
{
//register ports
addPort(inport_);
addPort(outport_);
//register properties
addProperty(prefixProp_);
}
|
The constructor of each implemented processor in Voreen looks nearly the same. Fist we call the base class constructor and initialize its members. In the function body itself we have to add the ports and properties by calling the specific add functions. It is needed to get a proper internal handling of ports and properties belonging to the processor.
Processor* SampleProcessor::create() const {
return new SampleProcessor();
}
std::string SampleProcessor::getClassName() const {
return "SampleProcessor";
}
std::string SampleProcessor::getCategory() const {
return "Text Processing";
}
void SampleProcessor::setDescriptions() {
setDescription("My sample processor which adds a " \
"user defined prefix to a given text.");
}
|
These four functions are always implemented in this way. Beside name changes nothing more has to be done.
void SampleProcessor::process() {
//get inport data
std::string inString = inport_.getData();
//get prefix string
std::string prefixString = prefixProp_.get();
//combine both strings
std::string outString = prefixString + inString;
//set outport data
outport_.setData(outString);
}
} // namespace
|
This is the implementation of the "main" processor function. First we get our needed parameters from the inports and properties. Then we process them, e.g. combining both strings. Afterwards we fill our outports and the SampleProcessor is ready to use.
Sample Render Processor
After introducing the first simple processor we now want to make it a little bit more complex. This time we will render images via a GLSL shader in the SampleRenderProcessor. The processor and the according workspace "SampleRenderProcessor_workspace.vws" from the SampleModule are illustrated in the image above. The SampleRenderProcessor gets an image and modifies the saturation of the colors. The saturation can be manipulated by the user via a FloatProperty on the right-hand side. A saturation of zero equals a black-white image as shown in the Canvas. The colorful tooltip shows the original image in the ImageSource outport to validate the proper work of the SampleRenderProcessor.
samplerenderprocessor.h
#ifndef VRN_SAMPLERENDERPROCESSOR_H
#define VRN_SAMPLERENDERPROCESSOR_H
//header of base class
#include "voreen/core/processors/renderprocessor.h"
//port headers
#include "voreen/core/ports/renderport.h"
//property headers
#include "voreen/core/properties/floatproperty.h"
//header of the used shader object
#include "tgt/shadermanager.h"
//use namespace voreen
namespace voreen {
|
We include all needed headers and set the namespace to voreen.
/**
* Sample render processor for gray-scaling an input image using a user-defined parameter.
* VRN_CORE_API is a macro needed for shared libs on windows (see voreencoreapi.h)
*/
class VRN_CORE_API SampleRenderProcessor : public RenderProcessor {
public:
/**
* Constructor
*/
SampleRenderProcessor();
//------------------------------------------
// Pure virtual functions of base classes
//------------------------------------------
virtual Processor* create() const { return new SampleRenderProcessor(); }
virtual std::string getClassName() const { return "SampleRenderProcessor"; }
virtual std::string getCategory() const { return "Image Processing"; }
protected:
virtual void setDescriptions() { setDescription("Sample render processor for" \
"gray-scaling an input image."); }
virtual void process();
|
Here we are defining the constructor and implementing inline the four simple pure virtual functions of the base classes. Since our processor should render images and use RenderPorts it inherits from RenderProcessor. RenderProcessor is a subclass of Processor implementing specific functions for handling render targets internal.
/**
* Overwrites the base implementation of this function.
* It is used to load the needed shader.
* @see Processor
*/
virtual void initialize();
/**
* Overwrites the base implementation of this function.
* It is used to free the used shader.
* @see Processor
*/
virtual void deinitialize();
|
These two functions are new and were not used in the first example. They overwrite the basic implementation of the function in RenderProcessor or rather in Processor. They are used to load and free the needed shader.
private:
//-------------
// members
//-------------
RenderPort inport_; ///< input of the image which should be modified
RenderPort outport_; ///< output of the modified image
FloatProperty saturationProp_; ///< property for the color saturation parameter
tgt::Shader* shader_; ///< GLSL shader object used in process()
};
} // namespace
#endif // VRN_SAMPLERENDERPROCESSOR_H
|
The three first members are, except for the type, the same as in the SampleProcessor. The fourth is a shader object used to handle shaders in the process() function.
samplerenderprocessor.cpp
//header file
#include "samplerenderprocessor.h"
//needed headers (used in process())
#include "tgt/textureunit.h"
//we are in namespace voreen
namespace voreen {
|
Simply including headers and setting the namespace.
SampleRenderProcessor::SampleRenderProcessor()
: RenderProcessor()
, inport_(Port::INPORT, "inport", "Unmodified Image")
, outport_(Port::OUTPORT, "outport", "Modified Image")
, saturationProp_("saturation", "Saturation" ///< property ID and GUI-label
,0.5f ///< default value
,0.f,1.f) ///< min and max value
{
//register ports
addPort(inport_);
addPort(outport_);
//register properties
addProperty(saturationProp_);
}
|
The constructor is nearly the same as the one of the SampleProcessor. We want to point out again the inheritance of the RenderProcessor, which causes the call of RenderProcessor instead Processor constructor.
void SampleRenderProcessor::initialize() {
// call superclass function first
RenderProcessor::initialize();
// load fragment shader 'sample.frag'
shader_ = ShdrMgr.loadSeparate("passthrough.vert", "sample.frag",
generateHeader(), // see RenderProcessor
false); // do not set default flags
}
void SampleRenderProcessor::deinitialize() {
// free shader
ShdrMgr.dispose(shader_);
shader_ = 0;
// call superclass function last
RenderProcessor::deinitialize();
}
|
These are the implementations of the overwrite functions. The ShaderManager (ShdrMgr) is a singleton class handling all shaders. In the initialize function we load the used pair of vertex and fragment shader. The vertex shader is simply the one used in the OpenGL pipeline and the fragment shader will be shown later on. The function generateHeader() is implemented in RenderProcessor and sets the default shader headers i.e. shader parameters. Deinitialize simply frees the loaded shaders.
void SampleRenderProcessor::process() {
// activate and clear output render target
outport_.activateTarget();
outport_.clearTarget();
// bind input image to texture units
tgt::TextureUnit colorUnit, depthUnit;
inport_.bindTextures(colorUnit.getEnum(), depthUnit.getEnum());
// activate shader and pass data
shader_->activate();
setGlobalShaderParameters(shader_); // see RenderProcessor
// pass input image to shader
inport_.setTextureParameters(shader_, "textureParameters_");
shader_->setUniform("colorTex_", colorUnit.getUnitNumber());
shader_->setUniform("depthTex_", depthUnit.getUnitNumber());
// pass property value to shader
shader_->setUniform("saturation_", saturationProp_.get());
// render screen aligned quad to run the fragment shader
renderQuad(); //(see RenderProcessor)
// cleanup
shader_->deactivate();
outport_.deactivateTarget();
tgt::TextureUnit::setZeroUnit();
// check for OpenGL errors
LGL_ERROR; // see tgt_gl.h
}
} // namespace
|
Other than all other ports RenderPorts don't have a getData or setData function. To set the data of a render outport we have to activate its render target. After the rendering we have to deactivate it again. To get the data (image) of the inport, we have to bind its internal textures (see line 8). After everything is done, we can check for OpenGL errors appeared during the rendering with the LGL_ERROR macro.
sample.frag
//include shader libraries (shader modules)
//defines and functions for 2D textures
#include "modules/mod_sampler2d.frag"
//defines and functions for filtering
#include "modules/mod_filtering.frag"
//uniforms for the color and depth textures
uniform sampler2D colorTex_;
uniform sampler2D depthTex_;
//struct defined in mod_sampler2d.frag containing all needed parameters
uniform TextureParameters textureParameters_;
//the saturation (user-defined by a property)
uniform float saturation_;
|
In the first part of the fragment shader we include Voreen shader libraries (a.k.a. modules). They offer some basic functions and defines which are frequently used. Next we define all uniforms being needed for our shader.
/**
* Main function of the shader. It takes the color texture passed as a uniform
* and modifies the saturation.
*/
void main() {
// look up input color and depth value (see mod_sampler2d.frag)
vec4 color = textureLookup2Dscreen(colorTex_, textureParameters_, gl_FragCoord.xy);
float depth = textureLookup2Dscreen(depthTex_, textureParameters_, gl_FragCoord.xy).z;
// compute gray value (see mod_filtering.frag) and pass-through depth value
FragData0 = rgbToGrayScaleSaturated(color, saturation_);
gl_FragDepth = depth;
}
|
The main shader function simply gets the texture color of the actual position and modifies it by the user-defined saturation.