diff --git a/simgear/scene/CMakeLists.txt b/simgear/scene/CMakeLists.txt index ccbd9416..e9bb45a5 100644 --- a/simgear/scene/CMakeLists.txt +++ b/simgear/scene/CMakeLists.txt @@ -9,6 +9,7 @@ foreach( mylibfolder tgdb util tsync + viewer ) add_subdirectory(${mylibfolder}) diff --git a/simgear/scene/material/EffectCullVisitor.cxx b/simgear/scene/material/EffectCullVisitor.cxx index 22d1ab02..a016a455 100644 --- a/simgear/scene/material/EffectCullVisitor.cxx +++ b/simgear/scene/material/EffectCullVisitor.cxx @@ -34,8 +34,9 @@ namespace simgear using osgUtil::CullVisitor; -EffectCullVisitor::EffectCullVisitor(bool collectLights) : - _collectLights(collectLights) +EffectCullVisitor::EffectCullVisitor(bool collectLights, Effect *effectOverride) : + _collectLights(collectLights), + _effectOverride(effectOverride) { } @@ -61,12 +62,18 @@ void EffectCullVisitor::apply(osg::Geode& node) if (_collectLights && ( eg->getNodeMask() & MODELLIGHT_BIT ) ) { _lightList.push_back( eg ); } - Effect* effect = eg->getEffect(); + Effect *effect; + if (_effectOverride) { + effect = _effectOverride; + } else { + effect = eg->getEffect(); + if (!effect) { + CullVisitor::apply(node); + return; + } + } Technique* technique = 0; - if (!effect) { - CullVisitor::apply(node); - return; - } else if (!(technique = effect->chooseTechnique(&getRenderInfo()))) { + if (!(technique = effect->chooseTechnique(&getRenderInfo()))) { return; } // push the node's state. diff --git a/simgear/scene/material/EffectCullVisitor.hxx b/simgear/scene/material/EffectCullVisitor.hxx index ec8f223a..ef93baa3 100644 --- a/simgear/scene/material/EffectCullVisitor.hxx +++ b/simgear/scene/material/EffectCullVisitor.hxx @@ -29,11 +29,12 @@ class Texture2D; namespace simgear { +class Effect; class EffectGeode; class EffectCullVisitor : public osgUtil::CullVisitor { public: - EffectCullVisitor(bool collectLights = false); + EffectCullVisitor(bool collectLights = false, Effect *effectOverride = 0); EffectCullVisitor(const EffectCullVisitor&); virtual osgUtil::CullVisitor* clone() const; using osgUtil::CullVisitor::apply; @@ -48,6 +49,7 @@ private: std::map > _bufferList; std::vector > _lightList; bool _collectLights; + osg::ref_ptr _effectOverride; }; } #endif diff --git a/simgear/scene/viewer/CMakeLists.txt b/simgear/scene/viewer/CMakeLists.txt new file mode 100644 index 00000000..df57469b --- /dev/null +++ b/simgear/scene/viewer/CMakeLists.txt @@ -0,0 +1,16 @@ +set(HEADERS + ClusteredForward.hxx + Compositor.hxx + CompositorCommon.hxx + CompositorBuffer.hxx + CompositorPass.hxx + ) + +set(SOURCES + ClusteredForward.cxx + Compositor.cxx + CompositorBuffer.cxx + CompositorPass.cxx + ) + +simgear_scene_component(viewer scene/viewer "${SOURCES}" "${HEADERS}") diff --git a/simgear/scene/viewer/ClusteredForward.cxx b/simgear/scene/viewer/ClusteredForward.cxx new file mode 100644 index 00000000..9235a71f --- /dev/null +++ b/simgear/scene/viewer/ClusteredForward.cxx @@ -0,0 +1,209 @@ +// Copyright (C) 2018 Fernando García Liñán +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +#include "ClusteredForward.hxx" + +#include +#include +#include +#include +#include + +namespace simgear { +namespace compositor { + +///// BEGIN DEBUG +#define DATA_SIZE 24 +const GLfloat LIGHT_DATA[DATA_SIZE] = { + 0.0, 0.0, -10.0, 1.0, 1.0, 0.0, 0.0, 1.0, + 0.0, 0.0, 10.0, 1.0, 0.0, 1.0, 0.0, 1.0, + 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0 +}; + +#define MAX_LIGHT_INDICES 4096 +#define MAX_POINT_LIGHTS 256 + +struct Light { + osg::Vec3 position; + float range; +}; + +#define NUM_LIGHTS 2 +Light LIGHT_LIST[NUM_LIGHTS] = { + {osg::Vec3(0.0f, 0.0f, -10.0f), 10.0f}, + {osg::Vec3(0.0f, 0.0f, 5.0f), 1000.0f} +}; +///// END DEBUG + +ClusteredForwardDrawCallback::ClusteredForwardDrawCallback() : + _initialized(false), + _tile_size(64), + _light_grid(new osg::Image), + _light_indices(new osg::Image), + _light_data(new osg::FloatArray(MAX_POINT_LIGHTS)) +{ +} + +void +ClusteredForwardDrawCallback::operator()(osg::RenderInfo &renderInfo) const +{ + osg::Camera *camera = renderInfo.getCurrentCamera(); + const osg::Viewport *vp = camera->getViewport(); + const int width = vp->width(); + const int height = vp->height(); + + // Round up + int n_htiles = (width + _tile_size - 1) / _tile_size; + int n_vtiles = (height + _tile_size - 1) / _tile_size; + + if (!_initialized) { + // Create and associate the light grid 3D texture + _light_grid->allocateImage(n_htiles, n_vtiles, 1, + GL_RGB_INTEGER, GL_UNSIGNED_SHORT); + _light_grid->setInternalTextureFormat(GL_RGB16UI); + + osg::ref_ptr light_grid_tex = new osg::Texture3D; + light_grid_tex->setResizeNonPowerOfTwoHint(false); + light_grid_tex->setWrap(osg::Texture3D::WRAP_R, osg::Texture3D::CLAMP_TO_BORDER); + light_grid_tex->setWrap(osg::Texture3D::WRAP_S, osg::Texture3D::CLAMP_TO_BORDER); + light_grid_tex->setWrap(osg::Texture3D::WRAP_T, osg::Texture3D::CLAMP_TO_BORDER); + light_grid_tex->setFilter(osg::Texture3D::MIN_FILTER, osg::Texture3D::NEAREST); + light_grid_tex->setFilter(osg::Texture3D::MAG_FILTER, osg::Texture3D::NEAREST); + light_grid_tex->setImage(0, _light_grid.get()); + + camera->getOrCreateStateSet()->setTextureAttributeAndModes( + 10, light_grid_tex.get(), osg::StateAttribute::ON); + + // Create and associate the light indices TBO + _light_indices->allocateImage(4096, 1, 1, GL_RED_INTEGER, GL_UNSIGNED_SHORT); + + osg::ref_ptr light_indices_tbo = + new osg::TextureBuffer; + light_indices_tbo->setInternalFormat(GL_R16UI); + light_indices_tbo->setImage(_light_indices.get()); + + camera->getOrCreateStateSet()->setTextureAttribute( + 11, light_indices_tbo.get()); + + // Create and associate the light data UBO + osg::ref_ptr light_data_ubo = + new osg::UniformBufferObject; + _light_data->setBufferObject(light_data_ubo.get()); + + osg::ref_ptr light_data_ubb = + new osg::UniformBufferBinding(0, light_data_ubo.get(), + 0, MAX_POINT_LIGHTS * 8 * sizeof(GLfloat)); + light_data_ubb->setDataVariance(osg::Object::DYNAMIC); + + camera->getOrCreateStateSet()->setAttribute( + light_data_ubb.get(), osg::StateAttribute::ON); + + _initialized = true; + } + + std::vector subfrustums; + const osg::Matrix &view_matrix = camera->getViewMatrix(); + const osg::Matrix &proj_matrix = camera->getProjectionMatrix(); + osg::Matrix view_proj_inverse = osg::Matrix::inverse(view_matrix * proj_matrix); + + double x_step = (_tile_size / width) * 2.0; + double y_step = (_tile_size / height) * 2.0; + for (int y = 0; y < n_vtiles; ++y) { + for (int x = 0; x < n_htiles; ++x) { + // Create the subfrustum in clip space + double x_min = -1.0 + x_step * x; double x_max = x_min + x_step; + double y_min = -1.0 + y_step * y; double y_max = y_min + y_step; + double z_min = 1.0; double z_max = -1.0; + osg::BoundingBox subfrustum_bb( + x_min, y_min, z_min, x_max, y_max, z_max); + osg::Polytope subfrustum; + subfrustum.setToBoundingBox(subfrustum_bb); + + // Transform it to world space + subfrustum.transformProvidingInverse(view_proj_inverse); + + subfrustums.push_back(subfrustum); + } + } + + GLushort *grid_data = reinterpret_cast + (_light_grid->data()); + GLushort *index_data = reinterpret_cast + (_light_indices->data()); + + GLushort global_light_count = 0; + for (size_t i = 0; i < subfrustums.size(); ++i) { + GLushort start_offset = global_light_count; + GLushort local_light_count = 0; + + for (GLushort light_list_index = 0; + light_list_index < NUM_LIGHTS; + ++light_list_index) { + const Light &light = LIGHT_LIST[light_list_index]; + osg::BoundingSphere bs(light.position, light.range); + + if (subfrustums[i].contains(bs)) { + index_data[global_light_count] = light_list_index; + ++local_light_count; + ++global_light_count; + } + } + grid_data[i * 3 + 0] = start_offset; + grid_data[i * 3 + 1] = local_light_count; + grid_data[i * 3 + 2] = 0; + } + + _light_grid->dirty(); + _light_indices->dirty(); + + // Upload light data + for (int i = 0; i < DATA_SIZE; ++i) { + (*_light_data)[i] = LIGHT_DATA[i]; + } + + // DEBUG + /* + if (!_debug) { + for (int y = 0; y < num_vtiles; ++y) { + for (int x = 0; x < num_htiles; ++x) { + std::cout << grid_data[(y * num_htiles + x) * 3 + 0] << "," + << grid_data[(y * num_htiles + x) * 3 + 1] << " "; + } + std::cout << std::endl; + } + std::cout << "\n\n"; + + for (int i = 0; i < num_vtiles * num_htiles; ++i) { + std::cout << index_data[i] << " "; + } + std::cout << "\n"; + _debug = true; + } + */ +/* + for (int y = 0; y < num_vtiles; ++y) { + for (int x = 0; x < num_htiles; ++x) { + data[(y * num_htiles + x) * 3 + 0] = (unsigned short)x; + data[(y * num_htiles + x) * 3 + 1] = (unsigned short)y; + data[(y * num_htiles + x) * 3 + 2] = 0; + } + } + _light_grid->dirty(); +*/ +} + +} // namespace compositor +} // namespace simgear diff --git a/simgear/scene/viewer/ClusteredForward.hxx b/simgear/scene/viewer/ClusteredForward.hxx new file mode 100644 index 00000000..8afa47b0 --- /dev/null +++ b/simgear/scene/viewer/ClusteredForward.hxx @@ -0,0 +1,40 @@ +// Copyright (C) 2018 Fernando García Liñán +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +#ifndef SG_CLUSTERED_FORWARD_HXX +#define SG_CLUSTERED_FORWARD_HXX + +#include + +namespace simgear { +namespace compositor { + +class ClusteredForwardDrawCallback : public osg::Camera::DrawCallback { +public: + ClusteredForwardDrawCallback(); + virtual void operator()(osg::RenderInfo &renderInfo) const; +protected: + mutable bool _initialized; + int _tile_size; + osg::ref_ptr _light_grid; + osg::ref_ptr _light_indices; + osg::ref_ptr _light_data; +}; + +} // namespace compositor +} // namespace simgear + +#endif /* SG_CLUSTERED_FORWARD_HXX */ diff --git a/simgear/scene/viewer/Compositor.cxx b/simgear/scene/viewer/Compositor.cxx new file mode 100644 index 00000000..8058d0d3 --- /dev/null +++ b/simgear/scene/viewer/Compositor.cxx @@ -0,0 +1,313 @@ +// Copyright (C) 2018 Fernando García Liñán +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +#include "Compositor.hxx" + +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace simgear { +namespace compositor { + +Compositor * +Compositor::create(osg::View *view, + osg::GraphicsContext *gc, + osg::Viewport *viewport, + const SGPropertyNode *property_list) +{ + osg::ref_ptr compositor = new Compositor(view, gc, viewport); + compositor->_name = property_list->getStringValue("name"); + + // Read all buffers first so passes can use them + PropertyList p_buffers = property_list->getChildren("buffer"); + for (auto const &p_buffer : p_buffers) { + const std::string &buffer_name = p_buffer->getStringValue("name"); + if (buffer_name.empty()) { + SG_LOG(SG_INPUT, SG_ALERT, "Compositor::build: Buffer requires " + "a name to be available to passes. Skipping..."); + continue; + } + Buffer *buffer = buildBuffer(compositor.get(), p_buffer); + if (buffer) + compositor->addBuffer(buffer_name, buffer); + } + // Read passes + PropertyList p_passes = property_list->getChildren("pass"); + for (auto const &p_pass : p_passes) { + Pass *pass = buildPass(compositor.get(), p_pass); + if (pass) + compositor->addPass(pass); + } + + return compositor.release(); +} + +Compositor * +Compositor::create(osg::View *view, + osg::GraphicsContext *gc, + osg::Viewport *viewport, + const std::string &name) +{ + std::string filename(name); + filename += ".xml"; + std::string abs_filename = SGModelLib::findDataFile(filename); + if (abs_filename.empty()) { + SG_LOG(SG_INPUT, SG_ALERT, "Compositor::build: Could not find file '" + << filename << "'"); + return 0; + } + + SGPropertyNode_ptr property_list = new SGPropertyNode; + try { + readProperties(abs_filename, property_list.ptr(), 0, true); + } catch (sg_io_exception &e) { + SG_LOG(SG_INPUT, SG_ALERT, "Compositor::build: Failed to parse file '" + << abs_filename << "'. " << e.getFormattedMessage()); + return 0; + } + + return create(view, gc, viewport, property_list); +} + +Compositor::Compositor(osg::View *view, + osg::GraphicsContext *gc, + osg::Viewport *viewport) : + _view(view), + _gc(gc), + _viewport(viewport), + _uniforms{ + new osg::Uniform("fg_ViewportSize", osg::Vec2f()), + new osg::Uniform("fg_ViewMatrix", osg::Matrixf()), + new osg::Uniform("fg_ViewMatrixInverse", osg::Matrixf()), + new osg::Uniform("fg_ProjectionMatrix", osg::Matrixf()), + new osg::Uniform("fg_ProjectionMatrixInverse", osg::Matrixf()), + new osg::Uniform("fg_CameraPositionCart", osg::Vec3f()), + new osg::Uniform("fg_CameraPositionGeod", osg::Vec3f()) + } +{ +} + +Compositor::~Compositor() +{ +} + +void +Compositor::update(const osg::Matrix &view_matrix, + const osg::Matrix &proj_matrix) +{ + for (auto &pass : _passes) { + if (pass->update_callback.valid()) + pass->update_callback->updatePass(*pass.get(), view_matrix, proj_matrix); + } + + // Update uniforms + osg::Matrixd view_inverse = osg::Matrix::inverse(view_matrix); + osg::Vec4d camera_pos = osg::Vec4(0.0, 0.0, 0.0, 1.0) * view_inverse; + SGGeod camera_pos_geod = SGGeod::fromCart( + SGVec3d(camera_pos.x(), camera_pos.y(), camera_pos.z())); + + for (int i = 0; i < TOTAL_BUILTIN_UNIFORMS; ++i) { + osg::ref_ptr u = _uniforms[i]; + switch (i) { + case VIEWPORT_SIZE: + u->set(osg::Vec2f(_viewport->width(), _viewport->height())); + break; + case VIEW_MATRIX: + u->set(view_matrix); + break; + case VIEW_MATRIX_INV: + u->set(view_inverse); + break; + case PROJECTION_MATRIX: + u->set(proj_matrix); + break; + case PROJECTION_MATRIX_INV: + u->set(osg::Matrix::inverse(proj_matrix)); + break; + case CAMERA_POSITION_CART: + u->set(osg::Vec3f(camera_pos.x(), camera_pos.y(), camera_pos.z())); + break; + case CAMERA_POSITION_GEOD: + u->set(osg::Vec3f(camera_pos_geod.getLongitudeRad(), + camera_pos_geod.getLatitudeRad(), + camera_pos_geod.getElevationM())); + break; + default: + // Unknown uniform + break; + } + } +} + +void +Compositor::resized() +{ + // Cameras attached directly to the framebuffer were already resized by + // osg::GraphicsContext::resizedImplementation(). However, RTT cameras were + // ignored. Here we resize RTT cameras that need to match the physical + // viewport size. + for (const auto &pass : _passes) { + osg::Camera *camera = pass->camera; + if (!camera->isRenderToTextureCamera() || + pass->viewport_width_scale == 0.0f || + pass->viewport_height_scale == 0.0f) + continue; + + // Resize both the viewport and its texture attachments + camera->resize(pass->viewport_width_scale * _viewport->width(), + pass->viewport_height_scale * _viewport->height()); + } +} + +void +Compositor::setCameraCullMasks(osg::Node::NodeMask nm) +{ + for (const auto &pass : _passes) { + osg::Camera *camera = pass->camera; + osg::Node::NodeMask pass_cm = nm; + // Disable traversal of the scene graph if the pass isn't enabled + if (!pass->isEnabled()) + pass_cm &= 0x0; + else + pass_cm &= pass->cull_mask; + + camera->setCullMask(pass_cm); + camera->setCullMaskLeft(pass_cm); + camera->setCullMaskRight(pass_cm); + } +} + +bool +Compositor::computeIntersection( + const osg::Vec2d& windowPos, + osgUtil::LineSegmentIntersector::Intersections& intersections) +{ + using osgUtil::Intersector; + using osgUtil::LineSegmentIntersector; + + osg::Camera *camera = getPass(0)->camera; + const osg::Viewport* viewport = camera->getViewport(); + SGRect viewportRect(viewport->x(), viewport->y(), + viewport->x() + viewport->width() - 1.0, + viewport->y() + viewport->height()- 1.0); + + double epsilon = 0.5; + if (!viewportRect.contains(windowPos.x(), windowPos.y(), epsilon)) + return false; + + osg::Vec4d start(windowPos.x(), windowPos.y(), 0.0, 1.0); + osg::Vec4d end(windowPos.x(), windowPos.y(), 1.0, 1.0); + osg::Matrix windowMat = viewport->computeWindowMatrix(); + osg::Matrix startPtMat = osg::Matrix::inverse(camera->getProjectionMatrix() + * windowMat); + osg::Matrix endPtMat = startPtMat; // no far camera + + start = start * startPtMat; + start /= start.w(); + end = end * endPtMat; + end /= end.w(); + osg::ref_ptr picker + = new LineSegmentIntersector(Intersector::VIEW, + osg::Vec3d(start.x(), start.y(), start.z()), + osg::Vec3d(end.x(), end.y(), end.z())); + osgUtil::IntersectionVisitor iv(picker.get()); + iv.setTraversalMask( simgear::PICK_BIT ); + + const_cast(camera)->accept(iv); + if (picker->containsIntersections()) { + intersections = picker->getIntersections(); + return true; + } + + return false; +} + +void +Compositor::addBuffer(const std::string &name, Buffer *buffer) +{ + _buffers[name] = buffer; +} + +void +Compositor::addPass(Pass *pass) +{ + if (!_view) { + SG_LOG(SG_GENERAL, SG_ALERT, "Compositor::addPass: Couldn't add camera " + "as a slave to the view. View doesn't exist!"); + return; + } + + _view->addSlave(pass->camera, pass->useMastersSceneData); + + // Install the Effect cull visitor + osgViewer::Renderer* renderer + = static_cast(pass->camera->getRenderer()); + for (int i = 0; i < 2; ++i) { + osgUtil::SceneView* sceneView = renderer->getSceneView(i); + + osg::ref_ptr identifier; + identifier = sceneView->getCullVisitor()->getIdentifier(); + + sceneView->setCullVisitor( + new EffectCullVisitor(false, pass->effect_override)); + sceneView->getCullVisitor()->setIdentifier(identifier.get()); + + identifier = sceneView->getCullVisitorLeft()->getIdentifier(); + sceneView->setCullVisitorLeft(sceneView->getCullVisitor()->clone()); + sceneView->getCullVisitorLeft()->setIdentifier(identifier.get()); + + identifier = sceneView->getCullVisitorRight()->getIdentifier(); + sceneView->setCullVisitorRight(sceneView->getCullVisitor()->clone()); + sceneView->getCullVisitorRight()->setIdentifier(identifier.get()); + } + + _passes.push_back(pass); +} + +Buffer * +Compositor::getBuffer(const std::string &name) const +{ + auto it = _buffers.find(name); + if (it == _buffers.end()) + return 0; + return it->second.get(); +} + +Pass * +Compositor::getPass(const std::string &name) const +{ + auto it = std::find_if(_passes.begin(), _passes.end(), + [&name](const osg::ref_ptr &p) { + return p->name == name; + }); + if (it == _passes.end()) + return 0; + return (*it); +} + +} // namespace compositor +} // namespace simgear diff --git a/simgear/scene/viewer/Compositor.hxx b/simgear/scene/viewer/Compositor.hxx new file mode 100644 index 00000000..25744efb --- /dev/null +++ b/simgear/scene/viewer/Compositor.hxx @@ -0,0 +1,140 @@ +// Copyright (C) 2018 Fernando García Liñán +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +#ifndef SG_COMPOSITOR_HXX +#define SG_COMPOSITOR_HXX + +#include +#include + +// For osgUtil::LineSegmentIntersector::Intersections, which is a typedef. +#include + +#include "CompositorBuffer.hxx" +#include "CompositorPass.hxx" + +class SGPropertyNode; + +namespace simgear { +namespace compositor { + +/** + * A Compositor manages the rendering pipeline of a single physical camera, + * usually via a property tree interface. + * + * The building blocks that define a Compositor are: + * - Buffers. They represent a zone of GPU memory. This is implemented in the + * form of an OpenGL texture, but any type of information can be stored + * (which can be useful in compute shaders for example). + * - Passes. They represent render operations. They can get buffers as input + * and they can output to other buffers. They are also integrated with the + * Effects framework, so the OpenGL internal state is configurable per pass. + */ +class Compositor : public osg::Referenced { +public: + enum BuiltinUniform { + VIEWPORT_SIZE = 0, + VIEW_MATRIX, + VIEW_MATRIX_INV, + PROJECTION_MATRIX, + PROJECTION_MATRIX_INV, + CAMERA_POSITION_CART, + CAMERA_POSITION_GEOD, + TOTAL_BUILTIN_UNIFORMS + }; + + Compositor(osg::View *view, + osg::GraphicsContext *gc, + osg::Viewport *viewport); + ~Compositor(); + + /** + * \brief Create a Compositor from a property tree. + * + * @param view The View where the passes will be added as slaves. + * @param gc The context where the internal osg::Cameras will draw on. + * @param viewport The viewport position and size inside the window. + * @param property_list A valid property list that describes the Compositor. + * @return A Compositor or a null pointer if there was an error. + */ + static Compositor *create(osg::View *view, + osg::GraphicsContext *gc, + osg::Viewport *viewport, + const SGPropertyNode *property_list); + /** + * \overload + * \brief Create a Compositor from a file. + * + * @param name Name of the compositor. The function will search for a file + * named .xml in $FG_ROOT. + */ + static Compositor *create(osg::View *view, + osg::GraphicsContext *gc, + osg::Viewport *viewport, + const std::string &name); + + void update(const osg::Matrix &view_matrix, + const osg::Matrix &proj_matrix); + + void resized(); + + void setCameraCullMasks(osg::Node::NodeMask nm); + + bool computeIntersection( + const osg::Vec2d& windowPos, + osgUtil::LineSegmentIntersector::Intersections& intersections); + + const osg::GraphicsContext *getGraphicsContext() const { return _gc; } + + const osg::Viewport *getViewport() const { return _viewport; } + + typedef std::array< + osg::ref_ptr, + TOTAL_BUILTIN_UNIFORMS> BuiltinUniforms; + const BuiltinUniforms &getUniforms() const { return _uniforms; } + + void addBuffer(const std::string &name, Buffer *buffer); + void addPass(Pass *pass); + + void setName(const std::string &name) { _name = name; } + const std::string &getName() const { return _name; } + + typedef std::unordered_map> BufferMap; + const BufferMap & getBufferMap() const { return _buffers; } + Buffer * getBuffer(const std::string &name) const; + + typedef std::vector> PassList; + const PassList & getPassList() const { return _passes; } + unsigned int getNumPasses() const { return _passes.size(); } + Pass * getPass(size_t index) const { return _passes[index]; } + Pass * getPass(const std::string &name) const; + +protected: + friend class PassBuilder; + + osg::View *_view; + osg::GraphicsContext *_gc; + osg::ref_ptr _viewport; + std::string _name; + BufferMap _buffers; + PassList _passes; + BuiltinUniforms _uniforms; +}; + +} // namespace compositor +} // namespace simgear + +#endif /* SG_COMPOSITOR_HXX */ diff --git a/simgear/scene/viewer/CompositorBuffer.cxx b/simgear/scene/viewer/CompositorBuffer.cxx new file mode 100644 index 00000000..b52b5530 --- /dev/null +++ b/simgear/scene/viewer/CompositorBuffer.cxx @@ -0,0 +1,213 @@ +// Copyright (C) 2018 Fernando García Liñán +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +#include "CompositorBuffer.hxx" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "Compositor.hxx" +#include "CompositorCommon.hxx" + +namespace simgear { +namespace compositor { + +struct BufferFormat { + GLint internal_format; + GLenum source_format; + GLenum source_type; +}; + +PropStringMap buffer_format_map { + {"rgb8", {GL_RGB8, GL_RGBA, GL_UNSIGNED_BYTE}}, + {"rgba8", {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE}}, + {"rgb16f", {GL_RGB16F, GL_RGBA, GL_FLOAT}}, + {"rgb32f", {GL_RGB32F, GL_RGBA, GL_FLOAT}}, + {"rgba16f", {GL_RGBA16F, GL_RGBA, GL_FLOAT}}, + {"rgba32f", {GL_RGBA32F, GL_RGBA, GL_FLOAT}}, + {"r32f", {GL_R32F, GL_RED, GL_FLOAT}}, + {"rg32f", {GL_RG32F, GL_RG, GL_FLOAT}}, + {"depth16", {GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT}}, + {"depth24", {GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_FLOAT}}, + {"depth32", {GL_DEPTH_COMPONENT32, GL_DEPTH_COMPONENT, GL_FLOAT}}, + {"depth32f", {GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT}}, + {"depth-stencil", {GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_FLOAT}} +}; + +PropStringMap wrap_mode_map = { + {"clamp", osg::Texture::CLAMP}, + {"clamp-to-edge", osg::Texture::CLAMP_TO_EDGE}, + {"clamp-to-border", osg::Texture::CLAMP_TO_BORDER}, + {"repeat", osg::Texture::REPEAT}, + {"mirror", osg::Texture::MIRROR} +}; + +PropStringMap filter_mode_map = { + {"linear", osg::Texture::LINEAR}, + {"linear-mipmap-linear", osg::Texture::LINEAR_MIPMAP_LINEAR}, + {"linear-mipmap-nearest", osg::Texture::LINEAR_MIPMAP_NEAREST}, + {"nearest", osg::Texture::NEAREST}, + {"nearest-mipmap-linear", osg::Texture::NEAREST_MIPMAP_LINEAR}, + {"nearest-mipmap-nearest", osg::Texture::NEAREST_MIPMAP_NEAREST} +}; + +PropStringMap shadow_texture_mode_map = { + {"luminance", osg::Texture::LUMINANCE}, + {"intensity", osg::Texture::INTENSITY}, + {"alpha", osg::Texture::ALPHA} +}; + +PropStringMap shadow_compare_func_map = { + {"never", osg::Texture::NEVER}, + {"less", osg::Texture::LESS}, + {"equal", osg::Texture::EQUAL}, + {"lequal", osg::Texture::LEQUAL}, + {"greater", osg::Texture::GREATER}, + {"notequal", osg::Texture::NOTEQUAL}, + {"gequal", osg::Texture::GEQUAL}, + {"always", osg::Texture::ALWAYS} +}; + +Buffer * +buildBuffer(Compositor *compositor, const SGPropertyNode *node) +{ + std::string type = node->getStringValue("type"); + if (type.empty()) { + SG_LOG(SG_INPUT, SG_ALERT, "buildBuffer: No type specified"); + return 0; + } + + osg::ref_ptr buffer = new Buffer; + osg::Texture *texture; + + int width; + if (node->getStringValue("width") == std::string("screen")) { + float w_scale = node->getFloatValue("screen-width-scale", 1.0f); + buffer->width_scale = w_scale; + width = w_scale * compositor->getViewport()->width(); + } else { + width = node->getIntValue("width"); + } + int height; + if (node->getStringValue("height") == std::string("screen")) { + float h_scale = node->getFloatValue("screen-height-scale", 1.0f); + buffer->height_scale = h_scale; + height = h_scale * compositor->getViewport()->height(); + } else { + height = node->getIntValue("height"); + } + int depth = node->getIntValue("depth"); + if (type == "1d") { + osg::Texture1D *tex1D = new osg::Texture1D; + tex1D->setTextureWidth(width); + texture = tex1D; + } else if (type == "2d") { + osg::Texture2D *tex2D = new osg::Texture2D; + tex2D->setTextureSize(width, height); + texture = tex2D; + } else if (type == "2d-array") { + osg::Texture2DArray *tex2D_array = new osg::Texture2DArray; + tex2D_array->setTextureSize(width, height, depth); + texture = tex2D_array; + } else if (type == "2d-multisample") { + osg::Texture2DMultisample *tex2DMS = new osg::Texture2DMultisample; + tex2DMS->setTextureSize(width, height); + tex2DMS->setNumSamples(node->getIntValue("num-samples", 0)); + texture = tex2DMS; + } else if (type == "3d") { + osg::Texture3D *tex3D = new osg::Texture3D; + tex3D->setTextureSize(width, height, depth); + texture = tex3D; + } else if (type == "rect") { + osg::TextureRectangle *tex_rect = new osg::TextureRectangle; + tex_rect->setTextureSize(width, height); + texture = tex_rect; + } else if (type == "cubemap") { + osg::TextureCubeMap *tex_cubemap = new osg::TextureCubeMap; + tex_cubemap->setTextureSize(width, height); + texture = tex_cubemap; + } else { + SG_LOG(SG_INPUT, SG_ALERT, "Unknown texture type '" << type << "'"); + return 0; + } + buffer->texture = texture; + + bool resize_npot = node->getBoolValue("resize-npot", false); + texture->setResizeNonPowerOfTwoHint(resize_npot); + + BufferFormat format; + if (findPropString(node, "format", format, buffer_format_map)) { + texture->setInternalFormat(format.internal_format); + texture->setSourceFormat(format.source_format); + texture->setSourceType(format.source_type); + } else { + texture->setInternalFormat(GL_RGBA); + SG_LOG(SG_INPUT, SG_WARN, "Unknown buffer format specified, using RGBA"); + } + + osg::Texture::FilterMode filter_mode = osg::Texture::LINEAR; + findPropString(node, "min-filter", filter_mode, filter_mode_map); + texture->setFilter(osg::Texture::MIN_FILTER, filter_mode); + findPropString(node, "mag-filter", filter_mode, filter_mode_map); + texture->setFilter(osg::Texture::MAG_FILTER, filter_mode); + + osg::Texture::WrapMode wrap_mode = osg::Texture::CLAMP_TO_BORDER; + findPropString(node, "wrap-s", wrap_mode, wrap_mode_map); + texture->setWrap(osg::Texture::WRAP_S, wrap_mode); + findPropString(node, "wrap-t", wrap_mode, wrap_mode_map); + texture->setWrap(osg::Texture::WRAP_T, wrap_mode); + findPropString(node, "wrap-r", wrap_mode, wrap_mode_map); + texture->setWrap(osg::Texture::WRAP_R, wrap_mode); + + float anis = node->getFloatValue("anisotropy", 1.0f); + texture->setMaxAnisotropy(anis); + + osg::Vec4f border_color(0.0f, 0.0f, 0.0f, 0.0f); + const SGPropertyNode *p_border_color = node->getChild("border-color"); + if (p_border_color) + border_color = toOsg(p_border_color->getValue()); + texture->setBorderColor(border_color); + + bool shadow_comparison = node->getBoolValue("shadow-comparison", false); + texture->setShadowComparison(shadow_comparison); + if (shadow_comparison) { + osg::Texture::ShadowTextureMode shadow_texture_mode = + osg::Texture::LUMINANCE; + findPropString(node, "shadow-texture-mode", + shadow_texture_mode, shadow_texture_mode_map); + texture->setShadowTextureMode(shadow_texture_mode); + + osg::Texture::ShadowCompareFunc shadow_compare_func = + osg::Texture::LEQUAL; + findPropString(node, "shadow-compare-func", + shadow_compare_func, shadow_compare_func_map); + texture->setShadowCompareFunc(shadow_compare_func); + } + + return buffer.release(); +} + +} // namespace compositor +} // namespace simgear diff --git a/simgear/scene/viewer/CompositorBuffer.hxx b/simgear/scene/viewer/CompositorBuffer.hxx new file mode 100644 index 00000000..df0a92a4 --- /dev/null +++ b/simgear/scene/viewer/CompositorBuffer.hxx @@ -0,0 +1,46 @@ +// Copyright (C) 2018 Fernando García Liñán +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +#ifndef SG_COMPOSITOR_BUFFER_HXX +#define SG_COMPOSITOR_BUFFER_HXX + +#include + +class SGPropertyNode; + +namespace simgear { +namespace compositor { + +class Compositor; + +struct Buffer : public osg::Referenced { + Buffer() : width_scale(0.0f), height_scale(0.0f) {} + + osg::ref_ptr texture; + + /** + * The amount to multiply the size of the default framebuffer. + * A factor of 0.0 means that the buffer has a fixed size. + */ + float width_scale, height_scale; +}; + +Buffer *buildBuffer(Compositor *compositor, const SGPropertyNode *node); + +} // namespace compositor +} // namespace simgear + +#endif /* SG_COMPOSITOR_BUFFER_HXX */ diff --git a/simgear/scene/viewer/CompositorCommon.hxx b/simgear/scene/viewer/CompositorCommon.hxx new file mode 100644 index 00000000..17d519ae --- /dev/null +++ b/simgear/scene/viewer/CompositorCommon.hxx @@ -0,0 +1,62 @@ +// Copyright (C) 2018 Fernando García Liñán +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +#ifndef SG_COMPOSITOR_COMMON_HXX +#define SG_COMPOSITOR_COMMON_HXX + +#include + +namespace simgear { +namespace compositor { + +/** + * Lookup table that ties a string property value to a type that cannot be + * represented in the property tree. Useful for OSG or OpenGL enums. + */ +template +using PropStringMap = const std::unordered_map; + +template +bool findPropString(const std::string &str, + T &value, + const PropStringMap &map) +{ + auto itr = map.find(str); + if (itr == map.end()) + return false; + + value = itr->second; + return true; +} + +template +bool findPropString(const SGPropertyNode *parent, + const std::string &child_name, + T &value, + const PropStringMap &map) +{ + const SGPropertyNode *child = parent->getNode(child_name); + if (child) { + if (findPropString(child->getStringValue(), value, map)) + return true; + } + return false; +} + +} // namespace compositor +} // namespace simgear + +#endif /* SG_COMPOSITOR_COMMON_HXX */ diff --git a/simgear/scene/viewer/CompositorPass.cxx b/simgear/scene/viewer/CompositorPass.cxx new file mode 100644 index 00000000..d62a8a90 --- /dev/null +++ b/simgear/scene/viewer/CompositorPass.cxx @@ -0,0 +1,741 @@ +// Copyright (C) 2018 Fernando García Liñán +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +#include "CompositorPass.hxx" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "ClusteredForward.hxx" +#include "Compositor.hxx" +#include "CompositorCommon.hxx" + +namespace simgear { +namespace compositor { + +PropStringMap buffer_component_map = { + {"color", osg::Camera::COLOR_BUFFER}, + {"color0", osg::Camera::COLOR_BUFFER0}, + {"color1", osg::Camera::COLOR_BUFFER1}, + {"color2", osg::Camera::COLOR_BUFFER2}, + {"color3", osg::Camera::COLOR_BUFFER3}, + {"color4", osg::Camera::COLOR_BUFFER4}, + {"color5", osg::Camera::COLOR_BUFFER5}, + {"color6", osg::Camera::COLOR_BUFFER6}, + {"color7", osg::Camera::COLOR_BUFFER7}, + {"depth", osg::Camera::DEPTH_BUFFER}, + {"stencil", osg::Camera::STENCIL_BUFFER}, + {"packed-depth-stencil", osg::Camera::PACKED_DEPTH_STENCIL_BUFFER} +}; + +Pass * +PassBuilder::build(Compositor *compositor, const SGPropertyNode *root) +{ + // The pass index matches its render order + int render_order = root->getIndex(); + + osg::ref_ptr pass = new Pass; + pass->name = root->getStringValue("name"); + if (pass->name.empty()) { + SG_LOG(SG_INPUT, SG_WARN, "PassBuilder::build: Pass " << render_order + << " has no name. It won't be addressable by name!"); + } + pass->type = root->getStringValue("type"); + + const SGPropertyNode *condition = root->getChild("condition"); + if (condition) + pass->condition = sgReadCondition(getPropertyRoot(), condition); + + std::string eff_override_file = root->getStringValue("effect-override"); + if (!eff_override_file.empty()) + pass->effect_override = makeEffect(eff_override_file, true, 0); + + pass->cull_mask = std::stoul(root->getStringValue("cull-mask", "0xffffff"), + nullptr, 0); + + osg::Camera *camera = new Camera; + pass->camera = camera; + + camera->setName(pass->name); + camera->setGraphicsContext(compositor->_gc); + // Even though this camera will be added as a slave to the view, it will + // always be updated manually in Compositor::update() + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + // Same with the projection matrix + camera->setProjectionResizePolicy(osg::Camera::FIXED); + camera->setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); + + osg::Vec4f clear_color(0.0f, 0.0f, 0.0f, 0.0f); + const SGPropertyNode *p_clear_color = root->getChild("clear-color"); + if (p_clear_color) + clear_color = toOsg(p_clear_color->getValue()); + camera->setClearColor(clear_color); + osg::Vec4f clear_accum(0.0f, 0.0f, 0.0f, 0.0f); + const SGPropertyNode *p_clear_accum = root->getChild("clear-accum"); + if (p_clear_accum) + clear_accum = toOsg(p_clear_accum->getValue()); + camera->setClearAccum(clear_accum); + camera->setClearDepth(root->getFloatValue("clear-depth", 1.0f)); + camera->setClearStencil(root->getIntValue("clear-stencil", 0)); + + GLbitfield clear_mask = 0; + if (root->getBoolValue("clear-color-bit", true)) + clear_mask |= GL_COLOR_BUFFER_BIT; + if (root->getBoolValue("clear-accum-bit", false)) + clear_mask |= GL_ACCUM_BUFFER_BIT; + if (root->getBoolValue("clear-depth-bit", true)) + clear_mask |= GL_DEPTH_BUFFER_BIT; + if (root->getBoolValue("clear-stencil-bit", false)) + clear_mask |= GL_STENCIL_BUFFER_BIT; + // Default clear mask is GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, as in OSG + camera->setClearMask(clear_mask); + + PropertyList p_bindings = root->getChildren("binding"); + for (auto const &p_binding : p_bindings) { + try { + std::string buffer_name = p_binding->getStringValue("buffer"); + if (buffer_name.empty()) + throw sg_exception("No buffer specified"); + + Buffer *buffer = compositor->getBuffer(buffer_name); + if (!buffer) + throw sg_exception(std::string("Unknown buffer '") + + buffer_name + "'"); + + osg::Texture *texture = buffer->texture; + + int unit = p_binding->getIntValue("unit", -1); + if (unit < 0) + throw sg_exception("No texture unit specified"); + + // Make the texture available to every child of the pass, overriding + // existing units + camera->getOrCreateStateSet()->setTextureAttributeAndModes( + unit, + texture, + osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + } catch (sg_exception &e) { + SG_LOG(SG_INPUT, SG_ALERT, "PassBuilder::build: Skipping binding " + << p_binding->getIndex() << " in pass " << render_order + << ": " << e.what()); + } + } + + PropertyList p_attachments = root->getChildren("attachment"); + if (p_attachments.empty()) { + // If there are no attachments, assume the pass is rendering + // directly to the screen + + camera->setRenderOrder(osg::Camera::NESTED_RENDER, render_order * 10); + // OSG cameras use the framebuffer by default, but it is stated + // explicitly anyway + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER); + + camera->setDrawBuffer(GL_BACK); + camera->setReadBuffer(GL_BACK); + + // Use the physical viewport. We can't let the user choose the viewport + // size because some parts of the window might not be ours. + camera->setViewport(compositor->_viewport); + } else { + // This is a RTT camera + + camera->setRenderOrder(osg::Camera::PRE_RENDER, render_order * 10); + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + + bool viewport_absolute = false; + // The index of the attachment to be used as the size of the viewport. + // The one with index 0 is used by default. + int viewport_attachment = 0; + const SGPropertyNode *p_viewport = root->getChild("viewport"); + if (p_viewport) { + // The user has manually specified a viewport size + viewport_absolute = p_viewport->getBoolValue("absolute", false); + if (viewport_absolute) { + camera->setViewport(p_viewport->getIntValue("x"), + p_viewport->getIntValue("y"), + p_viewport->getIntValue("width"), + p_viewport->getIntValue("height")); + } + viewport_attachment = p_viewport->getIntValue("use-attachment", 0); + if (!root->getChild("attachment", viewport_attachment)) { + // Let OSG manage the viewport automatically + camera->setViewport(new osg::Viewport); + SG_LOG(SG_INPUT, SG_WARN, "PassBuilder::build: Can't use attachment " + << viewport_attachment << " to resize the viewport"); + } + } + + for (auto const &p_attachment : p_attachments) { + try { + std::string buffer_name = p_attachment->getStringValue("buffer"); + if (buffer_name.empty()) + throw sg_exception("No buffer specified"); + + Buffer *buffer = compositor->getBuffer(buffer_name); + if (!buffer) + throw sg_exception(std::string("Unknown buffer '") + + buffer_name + "'"); + + osg::Texture *texture = buffer->texture; + + osg::Camera::BufferComponent component = osg::Camera::COLOR_BUFFER; + findPropString(p_attachment, "component", component, buffer_component_map); + + unsigned int level = p_attachment->getIntValue("level", 0); + unsigned int face = p_attachment->getIntValue("face", 0); + bool mipmap_generation = + p_attachment->getBoolValue("mipmap-generation", false); + unsigned int multisample_samples = + p_attachment->getIntValue("multisample-samples", 0); + unsigned int multisample_color_samples = + p_attachment->getIntValue("multisample-color-samples", 0); + + camera->attach(component, + texture, + level, + face, + mipmap_generation, + multisample_samples, + multisample_color_samples); + + if (!viewport_absolute && + (p_attachment->getIndex() == viewport_attachment)) { + if ((buffer->width_scale == 0.0f) && + (buffer->height_scale == 0.0f)) { + // This is a fixed size pass. We allow the user to use + // relative coordinates to shape the viewport. + float x = p_viewport->getFloatValue("x", 0.0f); + float y = p_viewport->getFloatValue("y", 0.0f); + float width = p_viewport->getFloatValue("width", 1.0f); + float height = p_viewport->getFloatValue("height", 1.0f); + camera->setViewport(x * texture->getTextureWidth(), + y * texture->getTextureHeight(), + width * texture->getTextureWidth(), + height * texture->getTextureHeight()); + } else { + // This is a pass that should match the physical viewport + // size. Store the scales so we can resize the pass later + // if the physical viewport changes size. + pass->viewport_width_scale = buffer->width_scale; + pass->viewport_height_scale = buffer->height_scale; + camera->setViewport( + 0, + 0, + buffer->width_scale * compositor->_viewport->width(), + buffer->height_scale * compositor->_viewport->height()); + } + } + } catch (sg_exception &e) { + SG_LOG(SG_INPUT, SG_ALERT, "PassBuilder::build: Skipping attachment " + << p_attachment->getIndex() << " in pass " << render_order + << ": " << e.what()); + } + } + } + + return pass.release(); +} + +//------------------------------------------------------------------------------ + +struct QuadPassBuilder : public PassBuilder { +public: + virtual Pass *build(Compositor *compositor, const SGPropertyNode *root) { + osg::ref_ptr pass = PassBuilder::build(compositor, root); + pass->useMastersSceneData = false; + + osg::Camera *camera = pass->camera; + camera->setAllowEventFocus(false); + camera->setViewMatrix(osg::Matrix::identity()); + camera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1, 0, 1)); + + float left = 0.0f, bottom = 0.0f, width = 1.0f, height = 1.0f, scale = 1.0f; + const SGPropertyNode *p_geometry = root->getNode("geometry"); + if (p_geometry) { + left = p_geometry->getFloatValue("left", left); + bottom = p_geometry->getFloatValue("bottom", bottom); + width = p_geometry->getFloatValue("width", width); + height = p_geometry->getFloatValue("height", height); + scale = p_geometry->getFloatValue("scale", scale); + } + + const std::string eff_file = root->getStringValue("effect"); + + osg::ref_ptr quad = createFullscreenQuad( + left, bottom, width, height, scale, eff_file); + camera->addChild(quad); + + osg::StateSet *ss = camera->getOrCreateStateSet(); + for (const auto &uniform : compositor->getUniforms()) + ss->addUniform(uniform); + + return pass.release(); + } +protected: + osg::Geode *createFullscreenQuad(float left, + float bottom, + float width, + float height, + float scale, + const std::string &eff_file) { + osg::Geometry *geom; + + // When the quad is fullscreen, it can be optimized by using a + // a fullscreen triangle instead of a quad to avoid discarding pixels + // in the diagonal. If the desired geometry does not occupy the entire + // viewport, this optimization does not occur and a normal quad is drawn + // instead. + if (left != 0.0f || bottom != 0.0f || width != 1.0f || height != 1.0f + || scale != 1.0f) { + geom = osg::createTexturedQuadGeometry( + osg::Vec3(left, bottom, 0.0f), + osg::Vec3(width, 0.0f, 0.0f), + osg::Vec3(0.0f, height, 0.0f), + 0.0f, 0.0f, scale, scale); + } else { + geom = new osg::Geometry; + + osg::Vec3Array *coords = new osg::Vec3Array(3); + (*coords)[0].set(0.0f, 2.0f, 0.0f); + (*coords)[1].set(0.0f, 0.0f, 0.0f); + (*coords)[2].set(2.0f, 0.0f, 0.0f); + geom->setVertexArray(coords); + + osg::Vec2Array *tcoords = new osg::Vec2Array(3); + (*tcoords)[0].set(0.0f, 2.0f); + (*tcoords)[1].set(0.0f, 0.0f); + (*tcoords)[2].set(2.0f, 0.0f); + geom->setTexCoordArray(0, tcoords); + + osg::Vec4Array *colours = new osg::Vec4Array(1); + (*colours)[0].set(1.0f, 1.0f, 1.0, 1.0f); + geom->setColorArray(colours, osg::Array::BIND_OVERALL); + + osg::Vec3Array *normals = new osg::Vec3Array(1); + (*normals)[0].set(0.0f, 0.0f, 1.0f); + geom->setNormalArray(normals, osg::Array::BIND_OVERALL); + + geom->addPrimitiveSet(new osg::DrawArrays( + osg::PrimitiveSet::TRIANGLES, 0, 3)); + } + + osg::ref_ptr quad = new EffectGeode; + if (!eff_file.empty()) { + Effect *eff = makeEffect(eff_file, true, 0); + if (eff) + quad->setEffect(eff); + } + quad->addDrawable(geom); + quad->setCullingActive(false); + + osg::ref_ptr quad_state = quad->getOrCreateStateSet(); + int values = osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED; + quad_state->setAttribute(new osg::PolygonMode( + osg::PolygonMode::FRONT_AND_BACK, + osg::PolygonMode::FILL), + values); + quad_state->setMode(GL_LIGHTING, values); + quad_state->setMode(GL_DEPTH_TEST, values); + + return quad.release(); + } +}; +RegisterPassBuilder registerQuadPass("quad"); + +//------------------------------------------------------------------------------ + +#if 0 +class ShadowMapUpdateCallback; // Forward declaration + +class ShadowMapCullCallback : public osg::NodeCallback { +public: + ShadowMapCullCallback(int light_num, float near_m, float far_m) : + _light_num(light_num), + _near_m(near_m), + _far_m(far_m) {} + + virtual void operator()(osg::Node *node, osg::NodeVisitor *nv) { + osg::Camera *light_camera = static_cast(node); + osgUtil::CullVisitor *cv = dynamic_cast(nv); + + osg::Vec4 light_pos = _light->getPosition(); + if (light_pos.w() != 0.0) { + // We only support directional light sources for now + traverse(node, nv); + return; + } + + osg::Vec3 light_dir = + osg::Vec3(light_pos.x(), light_pos.y(), light_pos.z()); + + // Find the light + // Mostly taken from osgShadow + osgUtil::RenderStage *rs = cv->getRenderStage(); + osgUtil::PositionalStateContainer::AttrMatrixList &aml = + rs->getPositionalStateContainer()->getAttrMatrixList(); + for (auto &itr : aml) { + const osg::Light *light = dynamic_cast(itr.first.get()); + if (light) { + if (light->getLightNum() != _light_num) + continue; + + osg::Vec4 light_pos = light->getPosition(); + if (light_pos.w() == 0.0) { + // We only want directional lights + light_dir = osg::Vec3(light_pos.x(), light_pos.y(), light_pos.z()); + } + } + } + traverse(node, nv); + } +protected: + friend ShadowMapUpdateCallback; + int _light_num; + float _near_m; + float _far_m; + osg::Matrix _camera_view; + osg::Matrix _camera_proj; + osg::observer_ptr _light; + osg::ref_ptr _light_matrix_uniform; +}; +#endif + +class LightFinder : public osg::NodeVisitor { +public: + LightFinder(const std::string &name) : + osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN), + _name(name) {} + virtual void apply(osg::Node &node) { + // Only traverse the scene graph if we haven't found a light yet (or if + // the one we found earlier is no longer valid). + if (getLight().valid()) + return; + + if (node.getName() == _name) { + osg::LightSource *light_source = + dynamic_cast(&node); + if (light_source) + _light = light_source->getLight(); + } + + traverse(node); + } + osg::ref_ptr getLight() const { + osg::ref_ptr light_ref; + _light.lock(light_ref); + return light_ref; + } +protected: + std::string _name; + osg::observer_ptr _light; +}; + +struct ShadowMapUpdateCallback : public Pass::PassUpdateCallback { +public: + ShadowMapUpdateCallback(const std::string &light_name, + float near_m, float far_m, + const std::string &suffix, + int sm_width, int sm_height) : + _light_finder(new LightFinder(light_name)), + _near_m(near_m), + _far_m(far_m) { + _light_matrix_uniform = new osg::Uniform( + osg::Uniform::FLOAT_MAT4, std::string("fg_LightMatrix_") + suffix); + _half_sm_size = osg::Vec2d((double)sm_width, (double)sm_height) / 2.0; + } + virtual void updatePass(Pass &pass, + const osg::Matrix &view_matrix, + const osg::Matrix &proj_matrix) { + osg::Camera *camera = pass.camera; + // Look for the light + camera->accept(*_light_finder); + osg::ref_ptr light = _light_finder->getLight(); + if (!light) { + // We could not find any light + return; + } + osg::Vec4 light_pos = light->getPosition(); + if (light_pos.w() != 0.0) { + // We only support directional light sources for now + return; + } + osg::Vec3 light_dir = + osg::Vec3(light_pos.x(), light_pos.y(), light_pos.z()); + + // The light direction we've just queried is from the previous frame. + // This is because the position of the osg::LightSource gets updated + // during the update traversal, and this function happens before that + // in the SubsystemMgr update. + // This is not a problem though (for now). + + osg::Matrix view_inverse = osg::Matrix::inverse(view_matrix); + + // Calculate the light's point of view transformation matrices. + // Taken from Project Rembrandt. + double left, right, bottom, top, zNear, zFar; + proj_matrix.getFrustum(left, right, bottom, top, zNear, zFar); + + osg::BoundingSphere bs; + bs.expandBy(osg::Vec3(left, bottom, -zNear) * (_near_m / zNear)); + bs.expandBy(osg::Vec3(right, top, -zNear) * (_far_m / zNear)); + bs.expandBy(osg::Vec3(left, bottom, -zNear) * (_far_m / zNear)); + bs.expandBy(osg::Vec3(right, top, -zNear) * (_near_m / zNear)); + + osg::Vec4 aim4 = osg::Vec4(bs.center(), 1.0) * view_inverse; + osg::Vec3 aim(aim4.x(), aim4.y(), aim4.z()); + osg::Vec3 up(0.0f, 1.0f, 0.0f); + + osg::Matrixd &light_view_matrix = camera->getViewMatrix(); + light_view_matrix.makeLookAt( + aim + (light_dir * bs.radius() * 2.0f), + aim, + aim); + + osg::Matrixd &light_proj_matrix = camera->getProjectionMatrix(); + light_proj_matrix.makeOrtho( + -bs.radius(), bs.radius(), + -bs.radius(), bs.radius(), + -bs.radius() * 6.0f, bs.radius() * 6.0f); + + // Do texel snapping to prevent flickering or shimmering. + // We are using double precision vectors and matrices because in FG + // world coordinates are relative to the center of the Earth, which can + // (and will) cause precision issues due to their magnitude. + osg::Vec4d shadow_origin4 = osg::Vec4d(0.0, 0.0, 0.0, 1.0) * + light_view_matrix * light_proj_matrix; + osg::Vec2d shadow_origin(shadow_origin4.x(), shadow_origin4.y()); + shadow_origin = osg::Vec2d(shadow_origin.x() * _half_sm_size.x(), + shadow_origin.y() * _half_sm_size.y()); + osg::Vec2d rounded_origin(std::round(shadow_origin.x()), + std::round(shadow_origin.y())); + osg::Vec2d rounding = rounded_origin - shadow_origin; + rounding = osg::Vec2d(rounding.x() / _half_sm_size.x(), + rounding.y() / _half_sm_size.y()); + + osg::Matrixd round_matrix = osg::Matrixd::translate( + rounding.x(), rounding.y(), 0.0); + light_proj_matrix *= round_matrix; + + osg::Matrixf light_matrix = + // Include the real camera inverse view matrix because if the shader + // used world coordinates, there would be precision issues. + view_inverse * + camera->getViewMatrix() * + camera->getProjectionMatrix() * + // Bias matrices + osg::Matrix::translate(1.0, 1.0, 1.0) * + osg::Matrix::scale(0.5, 0.5, 0.5); + _light_matrix_uniform->set(light_matrix); + } + + osg::Uniform *getLightMatrixUniform() const { + return _light_matrix_uniform.get(); + } +protected: + osg::ref_ptr _light_finder; + float _near_m; + float _far_m; + osg::ref_ptr _light_matrix_uniform; + osg::Vec2d _half_sm_size; +}; + +struct ShadowMapPassBuilder : public PassBuilder { + virtual Pass *build(Compositor *compositor, const SGPropertyNode *root) { + osg::ref_ptr pass = PassBuilder::build(compositor, root); + + osg::Camera *camera = pass->camera; + camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF_INHERIT_VIEWPOINT); + + std::string light_name = root->getStringValue("light-name"); + float near_m = root->getFloatValue("near-m"); + float far_m = root->getFloatValue("far-m"); + int sm_width = camera->getViewport()->width(); + int sm_height = camera->getViewport()->height(); + pass->update_callback = new ShadowMapUpdateCallback( + light_name, + near_m, far_m, + pass->name, + sm_width, sm_height); + + return pass.release(); + } +}; +RegisterPassBuilder registerShadowMapPass("shadow-map"); + +//------------------------------------------------------------------------------ + +class SceneUpdateCallback : public Pass::PassUpdateCallback { +public: + SceneUpdateCallback(int cubemap_face, float zNear, float zFar) : + _cubemap_face(cubemap_face), + _zNear(zNear), + _zFar(zFar) {} + + virtual void updatePass(Pass &pass, + const osg::Matrix &view_matrix, + const osg::Matrix &proj_matrix) { + osg::Camera *camera = pass.camera; + if (_cubemap_face < 0) { + camera->setViewMatrix(view_matrix); + camera->setProjectionMatrix(proj_matrix); + } else { + osg::Vec3 camera_pos = osg::Vec3(0.0, 0.0, 0.0) * + osg::Matrix::inverse(view_matrix); + + typedef std::pair CubemapFace; + const CubemapFace id[] = { + CubemapFace(osg::Vec3( 1, 0, 0), osg::Vec3( 0, -1, 0)), // +X + CubemapFace(osg::Vec3(-1, 0, 0), osg::Vec3( 0, -1, 0)), // -X + CubemapFace(osg::Vec3( 0, 1, 0), osg::Vec3( 0, 0, 1)), // +Y + CubemapFace(osg::Vec3( 0, -1, 0), osg::Vec3( 0, 0, -1)), // -Y + CubemapFace(osg::Vec3( 0, 0, 1), osg::Vec3( 0, -1, 0)), // +Z + CubemapFace(osg::Vec3( 0, 0, -1), osg::Vec3( 0, -1, 0)) // -Z + }; + + osg::Matrix cubemap_view_matrix; + cubemap_view_matrix.makeLookAt(camera_pos, + camera_pos + id[_cubemap_face].first, + camera_pos + id[_cubemap_face].second); + camera->setViewMatrix(cubemap_view_matrix); + camera->setProjectionMatrixAsFrustum(-1.0, 1.0, -1.0, 1.0, + 1.0, 10000.0); + } + + if (_zNear != 0.0f && _zFar != 0.0f) { + osg::Matrix new_proj; + makeNewProjMat(camera->getProjectionMatrix(), + _zNear, _zFar, new_proj); + camera->setProjectionMatrix(new_proj); + } + } +protected: + // Given a projection matrix, return a new one with the same frustum + // sides and new near / far values. + void makeNewProjMat(Matrixd& oldProj, double znear, + double zfar, Matrixd& projection) { + projection = oldProj; + // Slightly inflate the near & far planes to avoid objects at the + // extremes being clipped out. + znear *= 0.999; + zfar *= 1.001; + + // Clamp the projection matrix z values to the range (near, far) + double epsilon = 1.0e-6; + if (fabs(projection(0,3)) < epsilon && + fabs(projection(1,3)) < epsilon && + fabs(projection(2,3)) < epsilon) { + // Projection is Orthographic + epsilon = -1.0/(zfar - znear); // Used as a temp variable + projection(2,2) = 2.0*epsilon; + projection(3,2) = (zfar + znear)*epsilon; + } else { + // Projection is Perspective + double trans_near = (-znear*projection(2,2) + projection(3,2)) / + (-znear*projection(2,3) + projection(3,3)); + double trans_far = (-zfar*projection(2,2) + projection(3,2)) / + (-zfar*projection(2,3) + projection(3,3)); + double ratio = fabs(2.0/(trans_near - trans_far)); + double center = -0.5*(trans_near + trans_far); + + projection.postMult(osg::Matrixd(1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, ratio, 0.0, + 0.0, 0.0, center*ratio, 1.0)); + } + } + + int _cubemap_face; + float _zNear; + float _zFar; +}; + +struct ScenePassBuilder : public PassBuilder { +public: + virtual Pass *build(Compositor *compositor, const SGPropertyNode *root) { + osg::ref_ptr pass = PassBuilder::build(compositor, root); + + osg::Camera *camera = pass->camera; + camera->setAllowEventFocus(true); + + const SGPropertyNode *clustered = root->getChild("clustered-forward"); + if (clustered) { + camera->setInitialDrawCallback(new ClusteredForwardDrawCallback); + } + + int cubemap_face = root->getIntValue("cubemap-face", -1); + float zNear = root->getFloatValue("z-near", 0.0f); + float zFar = root->getFloatValue("z-far", 0.0f); + pass->update_callback = new SceneUpdateCallback(cubemap_face, zNear, zFar); + + std::string shadow_pass_name = root->getStringValue("use-shadow-pass"); + if (!shadow_pass_name.empty()) { + Pass *shadow_pass = compositor->getPass(shadow_pass_name); + if (shadow_pass) { + ShadowMapUpdateCallback *updatecb = + dynamic_cast( + shadow_pass->update_callback.get()); + if (updatecb) { + camera->getOrCreateStateSet()->addUniform( + updatecb->getLightMatrixUniform()); + } else { + SG_LOG(SG_INPUT, SG_WARN, "ScenePassBuilder::build: Pass '" + << shadow_pass_name << "is not a shadow pass"); + } + } else { + SG_LOG(SG_INPUT, SG_WARN, "ScenePassBuilder::build: Could not " + "find shadow pass named '" << shadow_pass_name << "'"); + } + } + + return pass.release(); + } +}; + +RegisterPassBuilder registerScenePass("scene"); + +//------------------------------------------------------------------------------ + +Pass * +buildPass(Compositor *compositor, const SGPropertyNode *root) +{ + std::string type = root->getStringValue("type"); + if (type.empty()) { + SG_LOG(SG_INPUT, SG_ALERT, "buildPass: Unspecified pass type"); + return 0; + } + PassBuilder *builder = PassBuilder::find(type); + if (!builder) { + SG_LOG(SG_INPUT, SG_ALERT, "buildPass: Unknown pass type '" + << type << "'"); + return 0; + } + + return builder->build(compositor, root); +} + +} // namespace compositor +} // namespace simgear diff --git a/simgear/scene/viewer/CompositorPass.hxx b/simgear/scene/viewer/CompositorPass.hxx new file mode 100644 index 00000000..cb59a83c --- /dev/null +++ b/simgear/scene/viewer/CompositorPass.hxx @@ -0,0 +1,134 @@ +// Copyright (C) 2018 Fernando García Liñán +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +#ifndef SG_COMPOSITOR_PASS_HXX +#define SG_COMPOSITOR_PASS_HXX + +#include + +#include +#include + +#include +#include +#include +#include + +namespace simgear { +namespace compositor { + +class Compositor; + +/** + * A Pass encapsulates a single render operation. In an OSG context, this is + * best represented as a Camera attached to the Viewer as a slave camera. + * + * Passes can render directly to the framebuffer or to a texture via FBOs. Also, + * the OpenGL state can be modified via the Effects framework and by exposing RTT + * textures from previous passes. + * + * Every pass can be enabled and disabled via a property tree conditional + * expression. This allows dynamic rendering pipelines where features can be + * enabled or disabled in a coherent way by the user. + */ +struct Pass : public osg::Referenced { + Pass() : + useMastersSceneData(true), + cull_mask(0xffffff), + viewport_width_scale(0.0f), + viewport_height_scale(0.0f) {} + + bool isEnabled() const { return (condition == 0) || condition->test(); } + + std::string name; + std::string type; + osg::ref_ptr camera; + SGSharedPtr condition; + /** If null, there is no effect override for this pass. */ + osg::ref_ptr effect_override; + bool useMastersSceneData; + osg::Node::NodeMask cull_mask; + float viewport_width_scale; + float viewport_height_scale; + + struct PassUpdateCallback : public virtual osg::Referenced { + public: + virtual void updatePass(Pass &pass, + const osg::Matrix &view_matrix, + const osg::Matrix &proj_matrix) = 0; + }; + + osg::ref_ptr update_callback; +}; + +class PassBuilder : public SGReferenced { +public: + virtual ~PassBuilder() {} + + /** + * \brief Build a pass. + * + * By default, this function implements commonly used features such as + * input/output buffers, conditional support etc., but can be safely ignored + * and overrided for more special passes. + * + * @param compositor The Compositor instance that owns the pass. + * @param The root node of the pass property tree. + * @return A Pass or a null pointer if an error occurred. + */ + virtual Pass *build(Compositor *compositor, const SGPropertyNode *root); + + static PassBuilder *find(const std::string &type) { + auto itr = PassBuilderMapSingleton::instance()->_map.find(type); + if (itr == PassBuilderMapSingleton::instance()->_map.end()) + return 0; + return itr->second.ptr(); + } +protected: + typedef std::unordered_map> PassBuilderMap; + struct PassBuilderMapSingleton : public Singleton { + PassBuilderMap _map; + }; + template + friend struct RegisterPassBuilder; +}; + +/** + * An instance of this type registers a new pass type T with a name. + * A global instance of this class must be created in CompositorPass.cxx to + * register a new pass type. + */ +template +struct RegisterPassBuilder { + RegisterPassBuilder(const std::string &name) { + PassBuilder::PassBuilderMapSingleton::instance()-> + _map.insert(std::make_pair(name, new T)); + } +}; + +/** + * \brief Create a pass from a property tree definition. + * + * @param comp The Compositor instance that owns the pass. + * @param node The root node of the pass property tree. + * @return A Pass or a null pointer if an error occurred. + */ +Pass *buildPass(Compositor *compositor, const SGPropertyNode *root); + +} // namespace compositor +} // namespace simgear + +#endif /* SG_COMPOSITOR_PASS_HXX */