From 65483a373fbf1c929fdd024b23ac55f8206d77c1 Mon Sep 17 00:00:00 2001 From: Fahim Imaduddin Dalvi Date: Sat, 7 Aug 2021 21:47:56 +0300 Subject: [PATCH] Shader-based scenery lights. This commit implements shader-based scenery lights with support for efficient rendering of a large number of lights, animations and directionality. Detailed commit log can be found at [1]. Detailed discussions can be found on the mailing list [2][3][4]. [1] https://sourceforge.net/u/fahimdalvi/fgdata/ci/feat/scenery-shader-lights/~/tree/ [2] (Timed animation stops working in certain configurations) https://sourceforge.net/p/flightgear/mailman/flightgear-devel/thread/4A7AC359-63B3-4D8A-815B-09823BCEFF27%40gmail.com/#msg37095923 [3] (Shader-based scenery lights) https://sourceforge.net/p/flightgear/mailman/flightgear-devel/thread/63E3C131-7662-4F59-B2E6-03208C78EF96%40gmail.com/#msg37190517 [4] (Shader-based scenery lights Part 2) https://sourceforge.net/p/flightgear/mailman/flightgear-devel/thread/74F18179-CA34-4901-A44E-15498ECC230A%40gmail.com/#msg37331695 --- simgear/scene/tgdb/CMakeLists.txt | 2 + simgear/scene/tgdb/LightBin.cxx | 271 +++++++++++++++++++++++++ simgear/scene/tgdb/LightBin.hxx | 109 ++++++++++ simgear/scene/tgdb/ReaderWriterSTG.cxx | 89 +++++++- 4 files changed, 469 insertions(+), 2 deletions(-) create mode 100644 simgear/scene/tgdb/LightBin.cxx create mode 100644 simgear/scene/tgdb/LightBin.hxx diff --git a/simgear/scene/tgdb/CMakeLists.txt b/simgear/scene/tgdb/CMakeLists.txt index 44dcda9d..b734b5dd 100644 --- a/simgear/scene/tgdb/CMakeLists.txt +++ b/simgear/scene/tgdb/CMakeLists.txt @@ -5,6 +5,7 @@ set(HEADERS CoastlineBin.hxx GroundLightManager.hxx LineFeatureBin.hxx + LightBin.hxx ReaderWriterSPT.hxx ReaderWriterSTG.hxx SGBuildingBin.hxx @@ -36,6 +37,7 @@ set(SOURCES CoastlineBin.cxx GroundLightManager.cxx LineFeatureBin.cxx + LightBin.cxx ReaderWriterSPT.cxx ReaderWriterSTG.cxx SGBuildingBin.cxx diff --git a/simgear/scene/tgdb/LightBin.cxx b/simgear/scene/tgdb/LightBin.cxx new file mode 100644 index 00000000..476a6a90 --- /dev/null +++ b/simgear/scene/tgdb/LightBin.cxx @@ -0,0 +1,271 @@ +/* -*-c++-*- + * + * Copyright (C) 2020 Fahim Dalvi + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "LightBin.hxx" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace osg; +namespace simgear { + +void LightBin::insert(const Light& light) { + _aggregated_size += light.size; + _aggregated_intensity += light.intensity; + _lights.push_back(light); +} + +void LightBin::insert(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c) { + insert(Light(p, s, i, o, c)); +} +void LightBin::insert(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c, const SGVec4f& a) { + insert(Light(p, s, i, o, c, a)); +} +void LightBin::insert(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c, const SGVec3f& d, const double& ha, const double& va) { + insert(Light(p, s, i, o, c, d, ha, va)); +} +void LightBin::insert(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c, const SGVec3f& d, const double& ha, const double& va, const SGVec4f& a) { + insert(Light(p, s, i, o, c, d, ha, va, a)); +} + +unsigned LightBin::getNumLights() const +{ + return _lights.size(); +} + +const LightBin::Light& LightBin::getLight(unsigned i) const +{ + return _lights[i]; +} + +double LightBin::getAverageSize() const +{ + return _aggregated_size / getNumLights(); +} + +double LightBin::getAverageIntensity() const +{ + return _aggregated_intensity / getNumLights(); +} + +LightBin::LightBin(const SGPath& absoluteFileName) { + if (!absoluteFileName.exists()) { + SG_LOG(SG_TERRAIN, SG_ALERT, "Light list file " << absoluteFileName << " does not exist."); + return; + } + + sg_gzifstream stream(absoluteFileName); + if (!stream.is_open()) { + SG_LOG(SG_TERRAIN, SG_ALERT, "Unable to open " << absoluteFileName); + return; + } + + // Every light is defined by at most 19 properties denoting position, + // size, intensity, color, directionality and animations + float props[19]; + + while (!stream.eof()) { + // read a line. Each line defines a single light position and its properties, + // and may have a comment, starting with # + std::string line; + std::getline(stream, line); + + // strip comments + std::string::size_type hash_pos = line.find('#'); + if (hash_pos != std::string::npos) + line.resize(hash_pos); + + if (line.empty()) { + continue; // skip blank lines + } + + // and process further + std::stringstream in(line); + + int number_of_props = 0; + while(!in.eof()) { + in >> props[number_of_props++]; + } + + if (number_of_props == 10) { + // Omnidirectional + insert( + SGVec3f(props[0], props[1], props[2]), // position + props[3], // size + props[4], // intensity + props[5], // on_period + SGVec4f(props[6], props[7], props[8], props[9]) //color + ); + } else if (number_of_props == 14) { + // Omnidirectional Animated + insert( + SGVec3f(props[0], props[1], props[2]), // position + props[3], // size + props[4], // intensity + props[5], // on_period + SGVec4f(props[6], props[7], props[8], props[9]), // color + SGVec4f(props[10], props[11], props[12], props[13]) // animation_params + ); + } else if (number_of_props == 15) { + // Directional + insert( + SGVec3f(props[0], props[1], props[2]), // position + props[3], // size + props[4], // intensity + props[5], // on_period + SGVec4f(props[6], props[7], props[8], props[9]), // color + SGVec3f(props[10], props[11], props[12]), // direction + props[13], // horizontal_angle + props[14] // vertical_angle + ); + } else if (number_of_props == 19) { + // Directional Animated + insert( + SGVec3f(props[0], props[1], props[2]), // position + props[3], // size + props[4], // intensity + props[5], // on_period + SGVec4f(props[6], props[7], props[8], props[9]), // color + SGVec3f(props[10], props[11], props[12]), // direction + props[13], // horizontal_angle + props[14], // vertical_angle + SGVec4f(props[15], props[16], props[17], props[18]) // animation_params + ); + } else { + SG_LOG(SG_TERRAIN, SG_WARN, "Error parsing light entry in: " << absoluteFileName << " line: \"" << line << "\""); + continue; + } + } + + stream.close(); +} + +Effect* getLightEffect(double average_size, double average_intensity, const SGReaderWriterOptions* options) +{ + SGPropertyNode_ptr effectProp = new SGPropertyNode; + makeChild(effectProp, "inherits-from")->setStringValue("Effects/scenery-lights"); + + /** Add average size + intensity as uniforms for non-shader or + * non-point-sprite based lights. We cannot have per-light control using + * OpenGL's default lights GL_ARB_point_sprite/GL_ARB_point_parameters + * extensions, so we make a best-effort by making all lights in this bin + * the average size/intensity. + */ + + // Clamp intensity between 0 and 50000 candelas + average_intensity = min(max(0.0, average_intensity), 50000.0); + + // Scale size from cm to what OpenGL extensions expects + average_size = 1 + 50 * min(max(0.0, average_size), 500.0)/500.0; + + SGPropertyNode* params = makeChild(effectProp, "parameters"); + params->getNode("size",true)->setValue(average_size); + params->getNode("attenuation",true)->getNode("x", true)->setValue(1.0); + params->getNode("attenuation",true)->getNode("y", true)->setValue(0.00001); + params->getNode("attenuation",true)->getNode("z", true)->setValue(0.00000001); + params->getNode("min-size",true)->setValue(1.0f); + params->getNode("max-size",true)->setValue(average_size*(1+2*average_intensity/50000)); + params->getNode("cull-face",true)->setValue("off"); + + return makeEffect(effectProp, true, options); +} + +osg::Drawable* createDrawable(LightBin& lightList, const osg::Matrix& transform) { + if (lightList.getNumLights() <= 0) + return 0; + + osg::Vec3Array* vertices = new osg::Vec3Array; + osg::Vec4Array* colors = new osg::Vec4Array; + osg::Vec3Array* light_params = new osg::Vec3Array; + osg::Vec4Array* animation_params = new osg::Vec4Array; + osg::Vec3Array* direction_params_1 = new osg::Vec3Array; + osg::Vec2Array* direction_params_2 = new osg::Vec2Array; + + for (int lightIdx = 0; lightIdx < lightList.getNumLights(); lightIdx++) { + const auto l = lightList.getLight(lightIdx); + vertices->push_back(toOsg(l.position) * transform); + light_params->push_back(toOsg(SGVec3f(l.size, l.intensity, l.on_period))); + direction_params_1->push_back(toOsg(l.direction)); + direction_params_2->push_back(toOsg(SGVec2f(l.horizontal_angle, l.vertical_angle))); + animation_params->push_back(toOsg(l.animation_params)); + colors->push_back(toOsg(l.color)); + } + + osg::Geometry* geometry = new osg::Geometry; + geometry->setDataVariance(osg::Object::STATIC); + geometry->setVertexArray(vertices); + geometry->setVertexAttribArray(11, light_params, osg::Array::BIND_PER_VERTEX); + geometry->setVertexAttribArray(12, animation_params, osg::Array::BIND_PER_VERTEX); + geometry->setVertexAttribArray(13, direction_params_1, osg::Array::BIND_PER_VERTEX); + geometry->setVertexAttribArray(14, direction_params_2, osg::Array::BIND_PER_VERTEX); + geometry->setNormalBinding(osg::Geometry::BIND_OFF); + geometry->setColorArray(colors, osg::Array::BIND_PER_VERTEX); + + geometry->setComputeBoundingBoxCallback(new SGEnlargeBoundingBox(1)); + + osg::DrawArrays* drawArrays; + drawArrays = new osg::DrawArrays(osg::PrimitiveSet::POINTS, + 0, vertices->size()); + geometry->addPrimitiveSet(drawArrays); + + osg::StateSet* stateSet = geometry->getOrCreateStateSet(); + stateSet->setRenderBinDetails(POINT_LIGHTS_BIN, "DepthSortedBin"); + stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + osg::BlendFunc* blendFunc = new osg::BlendFunc; + stateSet->setAttribute(blendFunc); + stateSet->setMode(GL_BLEND, osg::StateAttribute::ON); + + osg::AlphaFunc* alphaFunc; + alphaFunc = new osg::AlphaFunc(osg::AlphaFunc::GREATER, 0.01); + stateSet->setAttribute(alphaFunc); + stateSet->setMode(GL_ALPHA_TEST, osg::StateAttribute::ON); + + return geometry; +} + +osg::ref_ptr createLights(LightBin& lightList, const osg::Matrix& transform, const SGReaderWriterOptions* options) { + osg::ref_ptr geode = new EffectGeode; + osg::ref_ptr lightEffect = getLightEffect(lightList.getAverageSize(), lightList.getAverageIntensity(), options); + geode->setEffect(lightEffect); + geode->addDrawable(createDrawable(lightList, transform)); + + // Set all light bits so that the lightbin is always on. Per light on/off + // control is provided by the shader. + geode->setNodeMask(LIGHTS_BITS | MODEL_BIT | PERMANENTLIGHT_BIT); + + return geode; +} + +}; \ No newline at end of file diff --git a/simgear/scene/tgdb/LightBin.hxx b/simgear/scene/tgdb/LightBin.hxx new file mode 100644 index 00000000..c3c47a7b --- /dev/null +++ b/simgear/scene/tgdb/LightBin.hxx @@ -0,0 +1,109 @@ +/* -*-c++-*- + * + * Copyright (C) 2020 Fahim Dalvi + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + +#ifndef LIGHT_BIN_HXX +#define LIGHT_BIN_HXX + +#include + +#include +#include + +namespace simgear +{ +class LightBin { +public: + struct Light { + // Omni-directional non-animated lights constructor + Light(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c) : + position(p), color(c), size(s), intensity(i), on_period(o), + direction(SGVec3f(0.0f, 0.0f, 0.0f)), horizontal_angle(360.0f), vertical_angle(360.0f), + animation_params(SGVec4f(-1.0f, 0.0f, 0.0f, 0.0f)) + { } + + // Omni-directional animated lights constructor + Light(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c, const SGVec4f& a) : + position(p), color(c), size(s), intensity(i), on_period(o), + direction(SGVec3f(0.0f, 0.0f, 0.0f)), horizontal_angle(360.0f), vertical_angle(360.0f), + animation_params(a) + { } + + // Directional non-animated lights constructor + Light(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c, const SGVec3f& d, const double& ha, const double& va) : + position(p), color(c), size(s), intensity(i), on_period(o), + direction(d), horizontal_angle(ha), vertical_angle(va), + animation_params(SGVec4f(-1.0f, 0.0f, 0.0f, 0.0f)) + { } + + // Directional animated lights constructor + Light(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c, const SGVec3f& d, const double& ha, const double& va, const SGVec4f& a) : + position(p), color(c), size(s), intensity(i), on_period(o), + direction(d), horizontal_angle(ha), vertical_angle(va), + animation_params(a) + { } + + // Basic Light parameters + SGVec3f position; + SGVec4f color; + double size, intensity; + int on_period; // 0 - all day, 1 - turn on exactly at sunset for night, 2 - turn on randomly around sunset, 3 - night + low visibility + + // Directionality parameters + SGVec3f direction; + double horizontal_angle, vertical_angle; // If both angles are 360, the light is treated as omni-directional + + /* + * animation_params defines the animation (if any) that the light has to be + * rendered with. It consists of 4 parameters: + * 1. animation_params[0] - interval: Length of the animation (in seconds) - i.e. the animation will loop every `interval` seconds. Animation is disabled if `interval` is less than equal to 0. + * 2. animation_params[1] - on_portion: Portion of the interval the light should functional, i.e. a value of 0.5 implies that the light will only function in the first half of the interval. + * 3. animation_params[2] - strobe_rate: Number of light strobes in the entire `interval`. A value of 1 implies the light will turn on and off once within an `interval`, 2 implies the light will turn on, turn off, turn on, turn off in one `interval`. A value less than or equal to 0 implies an always on light. + * 4. animation_params[3] - offset: Offset the interval start to allow for variety within multiple lights in the same scene, or specific multi-light animations like the RABBIT near the runway. A value less than 0 implies randomized offset. + */ + SGVec4f animation_params; + }; + + typedef std::vector LightList; + + LightBin() = default; + LightBin(const SGPath& absoluteFileName); + + ~LightBin() = default; + + void insert(const Light& light); + void insert(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c); + void insert(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c, const SGVec4f& a); + void insert(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c, const SGVec3f& d, const double& ha, const double& va); + void insert(const SGVec3f& p, const double& s, const double& i, const int& o, const SGVec4f& c, const SGVec3f& d, const double& ha, const double& va, const SGVec4f& a); + + unsigned getNumLights() const; + const Light& getLight(unsigned i) const; + double getAverageSize() const; + double getAverageIntensity() const; +private: + LightList _lights; + + double _aggregated_size, _aggregated_intensity; +}; + +osg::ref_ptr createLights(LightBin& lightList, const osg::Matrix& transform, const SGReaderWriterOptions* options); +}; +#endif diff --git a/simgear/scene/tgdb/ReaderWriterSTG.cxx b/simgear/scene/tgdb/ReaderWriterSTG.cxx index 75330633..82a69d27 100644 --- a/simgear/scene/tgdb/ReaderWriterSTG.cxx +++ b/simgear/scene/tgdb/ReaderWriterSTG.cxx @@ -56,7 +56,7 @@ #include #include #include - +#include #include @@ -73,6 +73,8 @@ #define LINE_FEATURE_LIST "LINE_FEATURE_LIST" #define AREA_FEATURE_LIST "AREA_FEATURE_LIST" #define COASTLINE_LIST "COASTLINE_LIST" +#define OBJECT_LIGHT "OBJECT_LIGHT" +#define LIGHT_LIST "LIGHT_LIST" namespace simgear { @@ -154,6 +156,21 @@ struct ReaderWriterSTG::_ModelBin { std::string _material_name; double _lon, _lat, _elev; }; + struct _Light { + _Light() : _lon(0), _lat(0), _elev(0), _size(0), _intensity(0), _on_period(0), _horizontal_angle(0), _vertical_angle(0) { } + double _lon, _lat, _elev; + double _size, _intensity; + int _on_period; + SGVec4f _color; + SGVec3f _direction; + double _horizontal_angle, _vertical_angle; + SGVec4f _animation_params; + }; + struct _LightList { + _LightList() : _lon(0), _lat(0), _elev(0) { } + std::string _filename; + double _lon, _lat, _elev; + }; struct _LineFeatureList { _LineFeatureList() { } @@ -334,6 +351,49 @@ struct ReaderWriterSTG::_ModelBin { } } + // Add lights + if (!_lightList.empty()) { + // Transform lights frame of ref for better precision + osg::MatrixTransform* matrixTransform; + matrixTransform = new osg::MatrixTransform(makeZUpFrame(_bucket.get_center())); + matrixTransform->setName("rotateLights"); + matrixTransform->setDataVariance(osg::Object::STATIC); + + LightBin lightList; + for (const auto& light : _lightList) { + osg::Matrix _position_frame(makeZUpFrame(SGGeod::fromDegM(light._lon, light._lat, light._elev))); + SGVec3f _position(_position_frame(3,0), _position_frame(3,1), _position_frame(3,2)); + + lightList.insert( + _position, + light._size, light._intensity, + light._on_period, + light._color, + light._direction, + light._horizontal_angle, light._vertical_angle, + light._animation_params + ); + } + + matrixTransform->addChild(createLights(lightList, matrixTransform->getInverseMatrix(), _options)); + group->addChild(matrixTransform); + } + + if (!_lightListList.empty()) { + for (const auto& ll : _lightListList) { + osg::MatrixTransform* matrixTransform; + matrixTransform = new osg::MatrixTransform(makeZUpFrame(SGGeod::fromDegM(ll._lon, ll._lat, ll._elev))); + matrixTransform->setName("rotateLights"); + matrixTransform->setDataVariance(osg::Object::STATIC); + + const auto path = SGPath(ll._filename); + LightBin lightList(path); + + matrixTransform->addChild(createLights(lightList, osg::Matrix::identity(), _options)); + group->addChild(matrixTransform); + } + } + return group.release(); } @@ -342,6 +402,8 @@ struct ReaderWriterSTG::_ModelBin { std::list<_Sign> _signList; std::list<_BuildingList> _buildingList; std::list<_TreeList> _treeList; + std::list<_Light> _lightList; + std::list<_LightList> _lightListList; /// The original options to use for this bunch of models osg::ref_ptr _options; @@ -653,6 +715,23 @@ struct ReaderWriterSTG::_ModelBin { coastFeaturelist._filename = path.utf8Str(); coastFeaturelist._bucket = bucketIndexFromFileName(absoluteFileName.file_base().c_str()); _coastFeatureListList.push_back(coastFeaturelist); + } else if (token == OBJECT_LIGHT) { + _Light light; + in >> light._lon >> light._lat >> light._elev + >> light._size >> light._intensity + >> light._on_period + >> light._color[0] >> light._color[1] >> light._color[2] >> light._color[3] + >> light._direction[0] >> light._direction[1] >> light._direction[2] + >> light._horizontal_angle >> light._vertical_angle + >> light._animation_params[0] >> light._animation_params[1] >> light._animation_params[2] >> light._animation_params[3]; + checkInsideBucket(absoluteFileName, light._lon, light._lat); + _lightList.push_back(light); + } else if (token == LIGHT_LIST) { + _LightList lightList; + lightList._filename = path.utf8Str(); + in >> lightList._lon >> lightList._lat >> lightList._elev; + checkInsideBucket(absoluteFileName, lightList._lon, lightList._lat); + _lightListList.push_back(lightList); } else { // Check registered callback for token. Keep lock until callback completed to make sure it will not be // executed after a thread successfully executed removeSTGObjectHandler() @@ -818,7 +897,9 @@ struct ReaderWriterSTG::_ModelBin { _treeListList.empty() && _lineFeatureListList.empty() && _areaFeatureListList.empty() && - _coastFeatureListList.empty()) + _coastFeatureListList.empty() && + _lightList.empty() && + _lightListList.empty()) { // The simple case, just return the terrain group return terrainGroup.release(); @@ -839,6 +920,8 @@ struct ReaderWriterSTG::_ModelBin { readFileCallback->_objectStaticList = _objectStaticList; readFileCallback->_buildingList = _buildingListList; readFileCallback->_treeList = _treeListList; + readFileCallback->_lightList = _lightList; + readFileCallback->_lightListList = _lightListList; readFileCallback->_signList = _signList; readFileCallback->_options = options; readFileCallback->_bucket = bucket; @@ -871,6 +954,8 @@ struct ReaderWriterSTG::_ModelBin { std::list<_LineFeatureList> _lineFeatureListList; std::list<_AreaFeatureList> _areaFeatureListList; std::list<_CoastlineList> _coastFeatureListList; + std::list<_Light> _lightList; + std::list<_LightList> _lightListList; }; ReaderWriterSTG::ReaderWriterSTG()