diff --git a/data/assets/examples/videoglobe.asset b/data/assets/examples/videoglobe.asset new file mode 100644 index 0000000000..3a9abe18fb --- /dev/null +++ b/data/assets/examples/videoglobe.asset @@ -0,0 +1,35 @@ +local earth = asset.require("scene/solarsystem/planets/earth/earth") + +local data = asset.syncedResource({ + Name = "Example Globe Video", + Type = "UrlSynchronization", + Identifier = "example_globe_video", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/test-video.mp4" +}) + +local layer = { + Identifier = "ExampleVideo", + Video = data .. "/test-video.mp4", + Name = "Example Video", + Enabled = true, + Type = "VideoTileLayer" +} + +asset.onInitialize(function() + openspace.globebrowsing.addLayer(earth.Earth.Identifier, "ColorLayers", layer) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteLayer(earth.Earth.Identifier, "ColorLayers", layer) +end) + +asset.export("layer", layer) + +asset.meta = { + Name = "Video Player Test", + Version = "1.0", + Description = "An example asset that shows how to include a video on Earth", + Author = "OpenSpace Team", + URL = "https://openspaceproject.com", + License = "MIT" +} diff --git a/data/assets/examples/videoplane.asset b/data/assets/examples/videoplane.asset new file mode 100644 index 0000000000..564ae9cfc9 --- /dev/null +++ b/data/assets/examples/videoplane.asset @@ -0,0 +1,50 @@ +local transforms = asset.require("scene/solarsystem/planets/earth/transforms") + +local data = asset.syncedResource({ + Name = "Example Globe Video", + Type = "UrlSynchronization", + Identifier = "example_globe_video", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/test-video.mp4" +}) + + +local plane = { + Identifier = "VideoPlaneExample", + Parent = transforms.EarthCenter.Identifier, + Transform = { + Translation = { + Type = "StaticTranslation", + Position = { 0.0, -11E7, 0.0 } + } + }, + Renderable = { + Type = "RenderableVideoPlane", + MirrorBackside = true, + Size = 3E7, + Video = data .. "/test-video.mp4", + PlaybackMode = "RealTimeLoop" + }, + GUI = { + Name = "Video Plane Example", + Path = "/Other/Planes" + } +} + +asset.onInitialize(function() + openspace.addSceneGraphNode(plane) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(plane) +end) + +asset.export(plane) + +asset.meta = { + Name = "Video Plane Test", + Version = "1.0", + Description = "An example asset that shows how to include a video on a plane", + Author = "OpenSpace Team", + URL = "https://openspaceproject.com", + License = "MIT" +} diff --git a/data/assets/examples/videoscreenspace.asset b/data/assets/examples/videoscreenspace.asset new file mode 100644 index 0000000000..22af9b67f4 --- /dev/null +++ b/data/assets/examples/videoscreenspace.asset @@ -0,0 +1,32 @@ +local data = asset.syncedResource({ + Name = "Example Globe Video", + Type = "UrlSynchronization", + Identifier = "example_globe_video", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/test-video.mp4" +}) + +local spec = { + Type = "ScreenSpaceVideo", + Identifier = "ScreenSpaceVideoExample", + Name = "Screen Space Video Example", + Video = data .. "/test-video.mp4" +}; + +asset.onInitialize(function() + openspace.addScreenSpaceRenderable(spec) +end) + +asset.onDeinitialize(function() + openspace.removeScreenSpaceRenderable(spec) +end) + +asset.export(spec) + +asset.meta = { + Name = "Video ScreenSpace", + Version = "1.0", + Description = "An example asset that shows how to include a video in screen space", + Author = "OpenSpace Team", + URL = "https://openspaceproject.com", + License = "MIT" +} diff --git a/data/assets/examples/videosphere.asset b/data/assets/examples/videosphere.asset new file mode 100644 index 0000000000..f7bc534b60 --- /dev/null +++ b/data/assets/examples/videosphere.asset @@ -0,0 +1,48 @@ +local data = asset.syncedResource({ + Name = "Example Globe Video", + Type = "UrlSynchronization", + Identifier = "example_globe_video", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/test-video.mp4" +}) + +local sphere = { + Identifier = "ExampleVideoOnSphere", + Transform = { + Translation = { + Type = "StaticTranslation", + Position = { 0, 0, 0 } + } + }, + Renderable = { + Type = "RenderableVideoSphere", + Enabled = true, + Size = 100.0, + Segments = 80, + Opacity = 1, + Video = data .. "/test-video.mp4", + Orientation = "Both", + }, + GUI = { + Name = "Video Sphere", + Path = "/Other/Spheres" + } +} + +asset.onInitialize(function() + openspace.addSceneGraphNode(sphere) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(sphere) +end) + +asset.export(sphere) + +asset.meta = { + Name = "Video Player on Sphere", + Version = "1.0", + Description = "An example asset that shows how to include a video on a sphere", + Author = "OpenSpace Team", + URL = "https://openspaceproject.com", + License = "MIT" +} diff --git a/data/assets/examples/videostretchedtotime.asset b/data/assets/examples/videostretchedtotime.asset new file mode 100644 index 0000000000..5da34c242c --- /dev/null +++ b/data/assets/examples/videostretchedtotime.asset @@ -0,0 +1,39 @@ +local earth = asset.require("scene/solarsystem/planets/earth/earth") + +local data = asset.syncedResource({ + Name = "Example Globe Video", + Type = "UrlSynchronization", + Identifier = "example_globe_video", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/test-video.mp4" +}) + +local spec = { + Identifier = "TestVideo", + Video = data .. "/test-video.mp4", + StartTime = '2023 01 29 20:00:00', + EndTime = '2023 01 29 21:00:00', + Name = "Test Video", + PlaybackMode = "MapToSimulationTime", + Enabled = asset.enabled, + Type = "VideoTileLayer", + Description = [[Video stretched to time]] +} + +asset.onInitialize(function() + openspace.globebrowsing.addLayer(earth.Earth.Identifier, "ColorLayers", spec) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteLayer(earth.Earth.Identifier, "ColorLayers", spec) +end) + +asset.export(spec) + +asset.meta = { + Name = "Video Stretched To Time", + Version = "1.0", + Description = "This is a video mapped to the simulation time in OpenSpace.", + Author = "OpenSpace Team", + URL = "https://openspaceproject.com", + License = "MIT" +} diff --git a/modules/base/rendering/renderableplane.cpp b/modules/base/rendering/renderableplane.cpp index 2ab38ca580..ab0d2dc5d1 100644 --- a/modules/base/rendering/renderableplane.cpp +++ b/modules/base/rendering/renderableplane.cpp @@ -41,6 +41,7 @@ #include #include #include +#include namespace { enum BlendMode { @@ -70,6 +71,13 @@ namespace { "This value specifies the size of the plane in meters" }; + constexpr openspace::properties::Property::PropertyInfo AutoScaleInfo = { + "AutoScale", + "Auto Scale", + "When true, the plane will automatically adjust in size to match the aspect " + "ratio of the content. Otherwise it will remain in the given size." + }; + constexpr openspace::properties::Property::PropertyInfo BlendModeInfo = { "BlendMode", "Blending Mode", @@ -91,7 +99,7 @@ namespace { std::optional mirrorBackside; // [[codegen::verbatim(SizeInfo.description)]] - float size; + std::variant size; enum class [[codegen::map(BlendMode)]] BlendMode { Normal, @@ -117,14 +125,21 @@ RenderablePlane::RenderablePlane(const ghoul::Dictionary& dictionary) , _blendMode(BlendModeInfo, properties::OptionProperty::DisplayType::Dropdown) , _billboard(BillboardInfo, false) , _mirrorBackside(MirrorBacksideInfo, false) - , _size(SizeInfo, 10.f, 0.f, 1e25f) + , _size(SizeInfo, glm::vec2(10.f), glm::vec2(0.f), glm::vec2(1e25f)) + , _autoScale(AutoScaleInfo, false) , _multiplyColor(MultiplyColorInfo, glm::vec3(1.f), glm::vec3(0.f), glm::vec3(1.f)) { Parameters p = codegen::bake(dictionary); addProperty(Fadeable::_opacity); - _size = p.size; + if (std::holds_alternative(p.size)) { + _size = glm::vec2(std::get(p.size)); + } + else { + _size = std::get(p.size); + } + _billboard = p.billboard.value_or(_billboard); _mirrorBackside = p.mirrorBackside.value_or(_mirrorBackside); @@ -164,9 +179,11 @@ RenderablePlane::RenderablePlane(const ghoul::Dictionary& dictionary) addProperty(_size); _size.onChange([this](){ _planeIsDirty = true; }); + addProperty(_autoScale); + addProperty(_multiplyColor); - setBoundingSphere(_size); + setBoundingSphere(glm::compMax(_size.value())); } bool RenderablePlane::isReady() const { @@ -298,15 +315,16 @@ void RenderablePlane::update(const UpdateData&) { } void RenderablePlane::createPlane() { - const GLfloat size = _size; + const GLfloat sizeX = _size.value().x; + const GLfloat sizeY = _size.value().y; const GLfloat vertexData[] = { - // x y z w s t - -size, -size, 0.f, 0.f, 0.f, 0.f, - size, size, 0.f, 0.f, 1.f, 1.f, - -size, size, 0.f, 0.f, 0.f, 1.f, - -size, -size, 0.f, 0.f, 0.f, 0.f, - size, -size, 0.f, 0.f, 1.f, 0.f, - size, size, 0.f, 0.f, 1.f, 1.f, + // x y z w s t + -sizeX, -sizeY, 0.f, 0.f, 0.f, 0.f, + sizeX, sizeY, 0.f, 0.f, 1.f, 1.f, + -sizeX, sizeY, 0.f, 0.f, 0.f, 1.f, + -sizeX, -sizeY, 0.f, 0.f, 0.f, 0.f, + sizeX, -sizeY, 0.f, 0.f, 1.f, 0.f, + sizeX, sizeY, 0.f, 0.f, 1.f, 1.f, }; glBindVertexArray(_quad); diff --git a/modules/base/rendering/renderableplane.h b/modules/base/rendering/renderableplane.h index 8dbddf81a9..feb6c53782 100644 --- a/modules/base/rendering/renderableplane.h +++ b/modules/base/rendering/renderableplane.h @@ -30,7 +30,7 @@ #include #include #include -#include +#include #include #include @@ -72,7 +72,8 @@ protected: properties::OptionProperty _blendMode; properties::BoolProperty _billboard; properties::BoolProperty _mirrorBackside; - properties::FloatProperty _size; + properties::Vec2Property _size; + properties::BoolProperty _autoScale; properties::Vec3Property _multiplyColor; ghoul::opengl::ProgramObject* _shader = nullptr; diff --git a/modules/base/rendering/renderableplaneimagelocal.cpp b/modules/base/rendering/renderableplaneimagelocal.cpp index fc6961a28d..401aa60c0f 100644 --- a/modules/base/rendering/renderableplaneimagelocal.cpp +++ b/modules/base/rendering/renderableplaneimagelocal.cpp @@ -93,6 +93,31 @@ RenderablePlaneImageLocal::RenderablePlaneImageLocal(const ghoul::Dictionary& di } }); } + + _autoScale.onChange([this]() { + if (!_autoScale) { + return; + } + + // Shape the plane based on the aspect ration of the image + glm::vec2 textureDim = glm::vec2(_texture->dimensions()); + if (_textureDimensions != textureDim) { + float aspectRatio = textureDim.x / textureDim.y; + float planeAspectRatio = _size.value().x / _size.value().y; + + if (std::abs(planeAspectRatio - aspectRatio) > + std::numeric_limits::epsilon()) + { + glm::vec2 newSize = + aspectRatio > 0.f ? + glm::vec2(_size.value().x * aspectRatio, _size.value().y) : + glm::vec2(_size.value().x, _size.value().y * aspectRatio); + _size = newSize; + } + + _textureDimensions = textureDim; + } + }); } bool RenderablePlaneImageLocal::isReady() const { @@ -162,6 +187,29 @@ void RenderablePlaneImageLocal::loadTexture() { _textureFile = std::make_unique(_texturePath.value()); _textureFile->setCallback([this]() { _textureIsDirty = true; }); + + if (!_autoScale) { + return; + } + + // Shape the plane based on the aspect ration of the image + glm::vec2 textureDim = glm::vec2(_texture->dimensions()); + if (_textureDimensions != textureDim) { + float aspectRatio = textureDim.x / textureDim.y; + float planeAspectRatio = _size.value().x / _size.value().y; + + if (std::abs(planeAspectRatio - aspectRatio) > + std::numeric_limits::epsilon()) + { + glm::vec2 newSize = + aspectRatio > 0.f ? + glm::vec2(_size.value().x * aspectRatio, _size.value().y) : + glm::vec2(_size.value().x, _size.value().y * aspectRatio); + _size = newSize; + } + + _textureDimensions = textureDim; + } } } diff --git a/modules/base/rendering/renderableplaneimagelocal.h b/modules/base/rendering/renderableplaneimagelocal.h index 44be33db83..e4d10349fd 100644 --- a/modules/base/rendering/renderableplaneimagelocal.h +++ b/modules/base/rendering/renderableplaneimagelocal.h @@ -58,6 +58,7 @@ private: properties::StringProperty _texturePath; ghoul::opengl::Texture* _texture = nullptr; + glm::vec2 _textureDimensions = glm::vec2(0.f); std::unique_ptr _textureFile; bool _isLoadingLazily = false; diff --git a/modules/base/rendering/renderableplaneimageonline.cpp b/modules/base/rendering/renderableplaneimageonline.cpp index 812a7f1711..bd127bef94 100644 --- a/modules/base/rendering/renderableplaneimageonline.cpp +++ b/modules/base/rendering/renderableplaneimageonline.cpp @@ -68,6 +68,31 @@ RenderablePlaneImageOnline::RenderablePlaneImageOnline( _texturePath.onChange([this]() { _textureIsDirty = true; }); _texturePath = p.url; addProperty(_texturePath); + + _autoScale.onChange([this]() { + if (!_autoScale) { + return; + } + + // Shape the plane based on the aspect ration of the image + glm::vec2 textureDim = glm::vec2(_texture->dimensions()); + if (_textureDimensions != textureDim) { + float aspectRatio = textureDim.x / textureDim.y; + float planeAspectRatio = _size.value().x / _size.value().y; + + if (std::abs(planeAspectRatio - aspectRatio) > + std::numeric_limits::epsilon()) + { + glm::vec2 newSize = + aspectRatio > 0.f ? + glm::vec2(_size.value().x * aspectRatio, _size.value().y) : + glm::vec2(_size.value().x, _size.value().y * aspectRatio); + _size = newSize; + } + + _textureDimensions = textureDim; + } + }); } void RenderablePlaneImageOnline::deinitializeGL() { @@ -132,6 +157,29 @@ void RenderablePlaneImageOnline::update(const UpdateData& data) { _texture = std::move(texture); _textureIsDirty = false; + + if (!_autoScale) { + return; + } + + // Shape the plane based on the aspect ration of the image + glm::vec2 textureDim = glm::vec2(_texture->dimensions()); + if (_textureDimensions != textureDim) { + float aspectRatio = textureDim.x / textureDim.y; + float planeAspectRatio = _size.value().x / _size.value().y; + + if (std::abs(planeAspectRatio - aspectRatio) > + std::numeric_limits::epsilon()) + { + glm::vec2 newSize = + aspectRatio > 0.f ? + glm::vec2(_size.value().x * aspectRatio, _size.value().y) : + glm::vec2(_size.value().x, _size.value().y * aspectRatio); + _size = newSize; + } + + _textureDimensions = textureDim; + } } } catch (const ghoul::io::TextureReader::InvalidLoadException& e) { diff --git a/modules/base/rendering/renderableplaneimageonline.h b/modules/base/rendering/renderableplaneimageonline.h index 6ef8676df2..66182bb749 100644 --- a/modules/base/rendering/renderableplaneimageonline.h +++ b/modules/base/rendering/renderableplaneimageonline.h @@ -60,6 +60,7 @@ private: std::future _imageFuture; std::unique_ptr _texture; + glm::vec2 _textureDimensions = glm::vec2(0.f); bool _textureIsDirty = false; }; diff --git a/modules/globebrowsing/CMakeLists.txt b/modules/globebrowsing/CMakeLists.txt index 31899a4ff0..c299869ea1 100644 --- a/modules/globebrowsing/CMakeLists.txt +++ b/modules/globebrowsing/CMakeLists.txt @@ -147,6 +147,7 @@ target_precompile_headers(${globebrowsing_module} PRIVATE ) +# Gdal install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/gdal_data DESTINATION modules/globebrowsing) if (WIN32) diff --git a/modules/globebrowsing/shaders/texturetilemapping.glsl b/modules/globebrowsing/shaders/texturetilemapping.glsl index dd70e3631e..74811b8331 100644 --- a/modules/globebrowsing/shaders/texturetilemapping.glsl +++ b/modules/globebrowsing/shaders/texturetilemapping.glsl @@ -166,6 +166,8 @@ vec4 getSample#{layerGroup}#{i}(vec2 uv, vec3 levelWeights, c.rgb = #{layerGroup}[#{i}].color; #elif (#{#{layerGroup}#{i}LayerType} == 9) // SpoutImageTileLayer c = getTexVal(#{layerGroup}[#{i}].pile, levelWeights, uv, #{layerGroup}[#{i}].padding); +#elif (#{#{layerGroup}#{i}LayerType} == 10) // VideoTileProvider + c = getTexVal(#{layerGroup}[#{i}].pile, levelWeights, uv, #{layerGroup}[#{i}].padding); #endif return c; diff --git a/modules/globebrowsing/src/gpulayergroup.cpp b/modules/globebrowsing/src/gpulayergroup.cpp index d6529ee642..1f27869a97 100644 --- a/modules/globebrowsing/src/gpulayergroup.cpp +++ b/modules/globebrowsing/src/gpulayergroup.cpp @@ -69,6 +69,7 @@ void GPULayerGroup::setValue(ghoul::opengl::ProgramObject& program, case layers::Layer::ID::DefaultTileLayer: case layers::Layer::ID::SingleImageTileLayer: case layers::Layer::ID::SpoutImageTileLayer: + case layers::Layer::ID::VideoTileLayer: case layers::Layer::ID::ImageSequenceTileLayer: case layers::Layer::ID::SizeReferenceTileLayer: case layers::Layer::ID::TemporalTileLayer: @@ -148,6 +149,7 @@ void GPULayerGroup::bind(ghoul::opengl::ProgramObject& p, const LayerGroup& laye case layers::Layer::ID::DefaultTileLayer: case layers::Layer::ID::SingleImageTileLayer: case layers::Layer::ID::SpoutImageTileLayer: + case layers::Layer::ID::VideoTileLayer: case layers::Layer::ID::ImageSequenceTileLayer: case layers::Layer::ID::SizeReferenceTileLayer: case layers::Layer::ID::TemporalTileLayer: diff --git a/modules/globebrowsing/src/layer.cpp b/modules/globebrowsing/src/layer.cpp index 6dc9c611ea..787278036c 100644 --- a/modules/globebrowsing/src/layer.cpp +++ b/modules/globebrowsing/src/layer.cpp @@ -115,7 +115,7 @@ namespace { std::optional type [[codegen::inlist("DefaultTileLayer", "SingleImageTileLayer", "ImageSequenceTileLayer", "SizeReferenceTileLayer", "TemporalTileLayer", "TileIndexTileLayer", "ByIndexTileLayer", - "ByLevelTileLayer", "SolidColor", "SpoutImageTileLayer")]]; + "ByLevelTileLayer", "SolidColor", "SpoutImageTileLayer", "VideoTileLayer")]]; // Determine whether the layer is enabled or not. If this value is not specified, // the layer is disabled @@ -319,6 +319,7 @@ Layer::Layer(layers::Group::ID id, const ghoul::Dictionary& layerDict, LayerGrou case layers::Layer::ID::TileIndexTileLayer: case layers::Layer::ID::ByIndexTileLayer: case layers::Layer::ID::ByLevelTileLayer: + case layers::Layer::ID::VideoTileLayer: if (_tileProvider) { removePropertySubOwner(*_tileProvider); } @@ -488,6 +489,7 @@ void Layer::initializeBasedOnType(layers::Layer::ID id, ghoul::Dictionary initDi case layers::Layer::ID::TileIndexTileLayer: case layers::Layer::ID::ByIndexTileLayer: case layers::Layer::ID::ByLevelTileLayer: + case layers::Layer::ID::VideoTileLayer: // We add the id to the dictionary since it needs to be known by // the tile provider initDict.setValue( @@ -499,6 +501,7 @@ void Layer::initializeBasedOnType(layers::Layer::ID id, ghoul::Dictionary initDi LDEBUG("Initializing tile provider for layer: '" + name + "'"); } _tileProvider = TileProvider::createFromDictionary(id, std::move(initDict)); + break; case layers::Layer::ID::SolidColor: if (initDict.hasValue(ColorInfo.identifier)) { @@ -522,6 +525,7 @@ void Layer::addVisibleProperties() { case layers::Layer::ID::TileIndexTileLayer: case layers::Layer::ID::ByIndexTileLayer: case layers::Layer::ID::ByLevelTileLayer: + case layers::Layer::ID::VideoTileLayer: if (_tileProvider) { addPropertySubOwner(*_tileProvider); } diff --git a/modules/globebrowsing/src/layergroupid.h b/modules/globebrowsing/src/layergroupid.h index 2feb9b1a2a..d6c4d55dfe 100644 --- a/modules/globebrowsing/src/layergroupid.h +++ b/modules/globebrowsing/src/layergroupid.h @@ -88,6 +88,7 @@ struct Layer { ByLevelTileLayer, SolidColor, SpoutImageTileLayer, + VideoTileLayer, Unknown }; @@ -95,7 +96,7 @@ struct Layer { std::string_view identifier; }; -constexpr std::array Layers = { +constexpr std::array Layers = { Layer { .id = Layer::ID::DefaultTileLayer, .identifier = "DefaultTileLayer" @@ -135,11 +136,13 @@ constexpr std::array Layers = { Layer { .id = Layer::ID::SpoutImageTileLayer, .identifier = "SpoutImageTileLayer" + }, + Layer { + .id = Layer::ID::VideoTileLayer, + .identifier = "VideoTileLayer" } }; - - struct Adjustment { enum class ID { None = 0, diff --git a/modules/globebrowsing/src/renderableglobe.cpp b/modules/globebrowsing/src/renderableglobe.cpp index a60413bf06..1c36d8d4f9 100644 --- a/modules/globebrowsing/src/renderableglobe.cpp +++ b/modules/globebrowsing/src/renderableglobe.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -817,6 +818,10 @@ void RenderableGlobe::render(const RenderData& data, RendererTasks& rendererTask } _lastChangedLayer = nullptr; + + // Reset + global::renderEngine->openglStateCache().resetBlendState(); + global::renderEngine->openglStateCache().resetDepthState(); } void RenderableGlobe::renderSecondary(const RenderData& data, RendererTasks&) { diff --git a/modules/globebrowsing/src/tileprovider/tileprovider.cpp b/modules/globebrowsing/src/tileprovider/tileprovider.cpp index ea7be68be1..01e76c4768 100644 --- a/modules/globebrowsing/src/tileprovider/tileprovider.cpp +++ b/modules/globebrowsing/src/tileprovider/tileprovider.cpp @@ -118,8 +118,8 @@ void TileProvider::deinitializeDefaultTile() { DefaultTileTexture = nullptr; } -TileProvider::TileProvider() - : properties::PropertyOwner({ "TileProvider", "Tile Provider"}) +TileProvider::TileProvider() + : properties::PropertyOwner({ "TileProvider", "Tile Provider" }) {} void TileProvider::initialize() { @@ -152,30 +152,10 @@ void TileProvider::deinitialize() { internalDeinitialize(); } -void TileProvider::internalInitialize() {} -void TileProvider::internalDeinitialize() {} - -ChunkTile TileProvider::chunkTile(TileIndex tileIndex, int parents, int maxParents) { - ZoneScoped; - - ghoul_assert(isInitialized, "TileProvider was not initialized"); - - auto ascendToParent = [](TileIndex& ti, TileUvTransform& uv) { - uv.uvOffset *= 0.5; - uv.uvScale *= 0.5; - - uv.uvOffset += ti.positionRelativeParent(); - - ti.x /= 2; - ti.y /= 2; - ti.level--; - }; - - TileUvTransform uvTransform = { - .uvOffset = glm::vec2(0.f, 0.f), - .uvScale = glm::vec2(1.f, 1.f) - }; - +ChunkTile TileProvider::traverseTree(TileIndex tileIndex, int parents, int maxParents, + std::function& ascendToParent, + TileUvTransform& uvTransform) +{ // Step 1. Traverse 0 or more parents up the chunkTree as requested by the caller for (int i = 0; i < parents && tileIndex.level > 1; i++) { ascendToParent(tileIndex, uvTransform); @@ -190,7 +170,7 @@ ChunkTile TileProvider::chunkTile(TileIndex tileIndex, int parents, int maxParen maxParents--; } if (maxParents < 0) { - return ChunkTile { Tile(), uvTransform, TileDepthTransform() }; + return ChunkTile{ Tile(), uvTransform, TileDepthTransform() }; } // Step 3. Traverse 0 or more parents up the chunkTree until we find a chunk that @@ -200,16 +180,44 @@ ChunkTile TileProvider::chunkTile(TileIndex tileIndex, int parents, int maxParen Tile t = tile(tileIndex); if (t.status != Tile::Status::OK) { if (--maxParents < 0) { - return ChunkTile { Tile(), uvTransform, TileDepthTransform() }; + return ChunkTile{ Tile(), uvTransform, TileDepthTransform() }; } ascendToParent(tileIndex, uvTransform); } else { - return ChunkTile { std::move(t), uvTransform, TileDepthTransform() }; + return ChunkTile{ std::move(t), uvTransform, TileDepthTransform() }; } } - return ChunkTile { Tile(), uvTransform, TileDepthTransform() }; + return ChunkTile{ Tile(), uvTransform, TileDepthTransform() }; +} + +void TileProvider::internalInitialize() {} +void TileProvider::internalDeinitialize() {} + +ChunkTile TileProvider::chunkTile(TileIndex tileIndex, int parents, int maxParents) { + ZoneScoped; + + ghoul_assert(isInitialized, "TileProvider was not initialized"); + + std::function ascendToParent = [] + (TileIndex& ti, TileUvTransform& uv) { + uv.uvOffset *= 0.5; + uv.uvScale *= 0.5; + + uv.uvOffset += ti.positionRelativeParent(); + + ti.x /= 2; + ti.y /= 2; + ti.level--; + }; + + TileUvTransform uvTransform = { + .uvOffset = glm::vec2(0.f, 0.f), + .uvScale = glm::vec2(1.f, 1.f) + }; + + return traverseTree(tileIndex, parents, maxParents, ascendToParent, uvTransform); } ChunkTilePile TileProvider::chunkTilePile(TileIndex tileIndex, int pileSize) { diff --git a/modules/globebrowsing/src/tileprovider/tileprovider.h b/modules/globebrowsing/src/tileprovider/tileprovider.h index bba6401412..ae76b241cb 100644 --- a/modules/globebrowsing/src/tileprovider/tileprovider.h +++ b/modules/globebrowsing/src/tileprovider/tileprovider.h @@ -57,6 +57,7 @@ namespace openspace::globebrowsing { namespace openspace::globebrowsing { +// If you add a new type, also add it to shaders/texturetilemapping.glsl enum class Type { DefaultTileProvider = 0, SingleImageTileProvider, @@ -66,10 +67,10 @@ enum class Type { TileIndexTileProvider, ByIndexTileProvider, ByLevelTileProvider, - InterpolateTileProvider + InterpolateTileProvider, + FfmpegTileProvider }; - struct TileProvider : public properties::PropertyOwner { static unsigned int NumTileProviders; @@ -131,7 +132,8 @@ struct TileProvider : public properties::PropertyOwner { virtual float noDataValueAsFloat() = 0; - ChunkTile chunkTile(TileIndex tileIndex, int parents = 0, int maxParents = 1337); + virtual ChunkTile chunkTile(TileIndex tileIndex, int parents = 0, + int maxParents = 1337); ChunkTilePile chunkTilePile(TileIndex tileIndex, int pileSize); @@ -139,6 +141,10 @@ struct TileProvider : public properties::PropertyOwner { uint16_t uniqueIdentifier = 0; bool isInitialized = false; +protected: + ChunkTile traverseTree(TileIndex tileIndex, int parents, int maxParents, + std::function& ascendToParent, + TileUvTransform& uvTransform); private: virtual void internalInitialize(); diff --git a/modules/skybrowser/src/renderableskytarget.cpp b/modules/skybrowser/src/renderableskytarget.cpp index 99425e6466..d4bd41ea79 100644 --- a/modules/skybrowser/src/renderableskytarget.cpp +++ b/modules/skybrowser/src/renderableskytarget.cpp @@ -117,6 +117,8 @@ RenderableSkyTarget::RenderableSkyTarget(const ghoul::Dictionary& dictionary) , _applyRoll(ApplyRollInfo, true) { // Handle target dimension property + _autoScale = false; + _autoScale.setReadOnly(true); const Parameters p = codegen::bake(dictionary); _crossHairSize = p.crossHairSize.value_or(_crossHairSize); @@ -165,12 +167,12 @@ glm::ivec3 RenderableSkyTarget::borderColor() const { } glm::dvec3 RenderableSkyTarget::rightVector() const { - double scaling = (_verticalFov / 70)* static_cast(_size.value()); + double scaling = (_verticalFov / 70)* static_cast(glm::compMax(_size.value())); return scaling * _rightVector; } glm::dvec3 RenderableSkyTarget::upVector() const { - double scaling = (_verticalFov / 70) * static_cast(_size.value()); + double scaling = (_verticalFov / 70) * static_cast(glm::compMax(_size.value())); return scaling * _upVector; } diff --git a/modules/video/CMakeLists.txt b/modules/video/CMakeLists.txt new file mode 100644 index 0000000000..059ec4f666 --- /dev/null +++ b/modules/video/CMakeLists.txt @@ -0,0 +1,107 @@ +########################################################################################## +# # +# OpenSpace # +# # +# Copyright (c) 2014-2023 # +# # +# Permission is hereby granted, free of charge, to any person obtaining a copy of this # +# software and associated documentation files (the "Software"), to deal in the Software # +# without restriction, including without limitation the rights to use, copy, modify, # +# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # +# permit persons to whom the Software is furnished to do so, subject to the following # +# conditions: # +# # +# The above copyright notice and this permission notice shall be included in all copies # +# or substantial portions of the Software. # +# # +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # +# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # +# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF # +# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE # +# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # +########################################################################################## + + +include(${PROJECT_SOURCE_DIR}/support/cmake/module_definition.cmake) + +set(HEADER_FILES + videomodule.h + include/videotileprovider.h + include/videoplayer.h + include/screenspacevideo.h + include/renderablevideosphere.h + include/renderablevideoplane.h +) +source_group("Header Files" FILES ${HEADER_FILES}) + +set(SOURCE_FILES + videomodule.cpp + src/videotileprovider.cpp + src/videoplayer.cpp + src/screenspacevideo.cpp + src/renderablevideosphere.cpp + src/renderablevideoplane.cpp +) +source_group("Source Files" FILES ${SOURCE_FILES}) + +# Libmpv +if (WIN32) + set(LIBMPV_ROOT "${CMAKE_CURRENT_BINARY_DIR}/ext/libmpv" CACHE INTERNAL "LIBMPV_ROOT") + if (NOT IS_DIRECTORY "${LIBMPV_ROOT}") + message(STATUS "Downloading LibMPV...") + + # First download the SHA1 hash of the file + file( + DOWNLOAD + "http://data.openspaceproject.com/ext/libmpv/2.0/libmpv.zip.sha1" + "${LIBMPV_ROOT}/libmpv.zip.sha1" + ) + file(READ "${LIBMPV_ROOT}/libmpv.zip.sha1" LIBMPV_SHA1) + + # For some reason the SHA1 file contains extra linebreaks + string(STRIP ${LIBMPV_SHA1} LIBMPV_SHA1) + + # Then download the file itself + file( + DOWNLOAD + "http://data.openspaceproject.com/ext/libmpv/2.0/libmpv.zip" + "${LIBMPV_ROOT}/libmpv.zip" + EXPECTED_HASH SHA1=${LIBMPV_SHA1} + SHOW_PROGRESS + ) + + execute_process( + COMMAND ${CMAKE_COMMAND} -E tar xzf "${LIBMPV_ROOT}/libmpv.zip" + WORKING_DIRECTORY ${LIBMPV_ROOT} + ) + endif () + + add_library(libmpv SHARED IMPORTED) + target_include_directories(libmpv SYSTEM INTERFACE ${LIBMPV_ROOT}/libmpv/include) + set_target_properties(libmpv PROPERTIES + IMPORTED_LOCATION "${LIBMPV_ROOT}/libmpv/bin/mpv-2.dll" + IMPORTED_IMPLIB "${LIBMPV_ROOT}/libmpv/lib/mpv.lib" + ) + + add_library(libopenh264 SHARED IMPORTED) + set_target_properties(libopenh264 PROPERTIES + IMPORTED_LOCATION "${LIBMPV_ROOT}/libmpv/bin/libopenh264.dll" + IMPORTED_IMPLIB "${LIBMPV_ROOT}/libmpv/lib/mpv.lib" + ) + + target_link_libraries(openspace-module-globebrowsing PUBLIC libmpv libopenh264) +else (WIN32) + find_package(LIBMPV REQUIRED) + + target_include_directories(openspace-module-globebrowsing SYSTEM PRIVATE ${LIBMPV_INCLUDE_DIR}) + target_link_libraries(openspace-module-globebrowsing PRIVATE ${LIBMPV_LIBRARY}) + mark_as_advanced(LIBMPV_CONFIG LIBMPV_INCLUDE_DIR LIBMPV_LIBRARY) +endif () # WIN32 + +create_new_module( + "Video" + video_module + STATIC + ${HEADER_FILES} ${SOURCE_FILES} +) diff --git a/modules/video/include.cmake b/modules/video/include.cmake new file mode 100644 index 0000000000..39d82b97ec --- /dev/null +++ b/modules/video/include.cmake @@ -0,0 +1,6 @@ +set(DEFAULT_MODULE ON) + +set(OPENSPACE_DEPENDENCIES + base + globebrowsing +) diff --git a/modules/video/include/renderablevideoplane.h b/modules/video/include/renderablevideoplane.h new file mode 100644 index 0000000000..d915f54632 --- /dev/null +++ b/modules/video/include/renderablevideoplane.h @@ -0,0 +1,58 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_BASE___RENDERABLEVIDEOPLANE___H__ +#define __OPENSPACE_MODULE_BASE___RENDERABLEVIDEOPLANE___H__ + +#include + +#include + +namespace openspace { + +namespace documentation { struct Documentation; } + +class RenderableVideoPlane : public RenderablePlane { +public: + RenderableVideoPlane(const ghoul::Dictionary& dictionary); + + void initializeGL() override; + void deinitializeGL() override; + + bool isReady() const override; + + void render(const RenderData& data, RendererTasks& rendererTask) override; + void update(const UpdateData& data) override; + +protected: + virtual void bindTexture() override; + +private: + VideoPlayer _videoPlayer; + glm::vec2 _textureDimensions = glm::vec2(0.f); +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_BASE___RENDERABLEVIDEOPLANE___H__ diff --git a/modules/video/include/renderablevideosphere.h b/modules/video/include/renderablevideosphere.h new file mode 100644 index 0000000000..006df3eafa --- /dev/null +++ b/modules/video/include/renderablevideosphere.h @@ -0,0 +1,94 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_BASE___RENDERABLEVIDEOSPHERE___H__ +#define __OPENSPACE_MODULE_BASE___RENDERABLEVIDEOSPHERE___H__ + +#include + +#include +#include +#include +#include +#include +#include + +namespace ghoul::opengl { + class ProgramObject; + class Texture; +} // namespace ghoul::opengl + +namespace openspace { + +class Sphere; +struct RenderData; +struct UpdateData; + +namespace documentation { struct Documentation; } + +class RenderableVideoSphere : public Renderable { +public: + RenderableVideoSphere(const ghoul::Dictionary& dictionary); + + void initializeGL() override; + void deinitializeGL() override; + + bool isReady() const override; + + virtual void render(const RenderData& data, RendererTasks& rendererTask) override; + virtual void update(const UpdateData& data) override; + + static documentation::Documentation Documentation(); + +protected: + void bindTexture(); + void unbindTexture(); + +private: + VideoPlayer _videoPlayer; + + properties::OptionProperty _orientation; + + properties::FloatProperty _size; + properties::IntProperty _segments; + + properties::BoolProperty _mirrorTexture; + properties::BoolProperty _disableFadeInDistance; + + properties::FloatProperty _fadeInThreshold; + properties::FloatProperty _fadeOutThreshold; + + ghoul::opengl::ProgramObject* _shader = nullptr; + + std::unique_ptr _sphere; + + UniformCache(opacity, modelViewProjection, modelViewRotation, colorTexture, + _mirrorTexture) _uniformCache; + + bool _sphereIsDirty = false; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_BASE___RENDERABLEVIDEOSPHERE___H__ diff --git a/modules/video/include/screenspacevideo.h b/modules/video/include/screenspacevideo.h new file mode 100644 index 0000000000..a864567aa7 --- /dev/null +++ b/modules/video/include/screenspacevideo.h @@ -0,0 +1,58 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_VIDEO___SCREENSPACEVIDEO___H__ +#define __OPENSPACE_MODULE_VIDEO___SCREENSPACEVIDEO___H__ + +#include + +#include +#include + +namespace ghoul::opengl { class Texture; } + +namespace openspace { + +namespace documentation { struct Documentation; } + +class ScreenSpaceVideo : public ScreenSpaceRenderable { +public: + ScreenSpaceVideo(const ghoul::Dictionary& dictionary); + + bool initializeGL() override; + bool deinitializeGL() override; + void update() override; + void render() override; + + static documentation::Documentation Documentation(); + +private: + void bindTexture() override; + + VideoPlayer _videoPlayer; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_VIDEO___SCREENSPACEVIDEO___H__ diff --git a/modules/video/include/videoplayer.h b/modules/video/include/videoplayer.h new file mode 100644 index 0000000000..1165bf2938 --- /dev/null +++ b/modules/video/include/videoplayer.h @@ -0,0 +1,165 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_VIDEO___VIDEOPLAYER___H__ +#define __OPENSPACE_MODULE_VIDEO___VIDEOPLAYER___H__ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +// libmpv +#include +#include + +namespace openspace { + +enum class PlaybackMode { + MapToSimulationTime = 0, + RealTimeLoop +}; + +class VideoPlayer : public properties::PropertyOwner, public Syncable { +BooleanType(PauseAfterSeek); + +public: + VideoPlayer(const ghoul::Dictionary& dictionary); + ~VideoPlayer(); + + void initialize(); + + // Video interaction + void pause(); + void play(); + void goToStart(); + + void seekToTime(double time, PauseAfterSeek pauseAfter = PauseAfterSeek::Yes); + void toggleMute(); + + const std::unique_ptr& frameTexture() const; + bool isInitialized() const; + + void reset(); + void destroy(); + void update(); + + virtual void preSync(bool isMaster) override; + virtual void encode(SyncBuffer* syncBuffer) override; + virtual void decode(SyncBuffer* syncBuffer) override; + virtual void postSync(bool isMaster) override; + + documentation::Documentation Documentation(); +private: + // Libmpv keys + enum class MpvKey : uint64_t { + Duration = 1, // 0 is the default key in libmpv so avoid that + Height, + Width, + Meta, + Params, + Time, + Fps, + Pause, + IsSeeking, + Mute, + Command, + Seek + }; + // Framebuffer + void createFBO(int width, int height); + void resizeFBO(int width, int height); + + // Libmpv + static void onMpvRenderUpdate(void*); // Has to be static because of C api + void initializeMpv(); // Called first time in update + void renderMpv(); // Called in update + void commandAsyncMpv(const char* cmd[], MpvKey key = MpvKey::Command); + void handleMpvEvents(); + // Libmpv properties + void handleMpvProperties(mpv_event* event); + void observePropertyMpv(MpvKey key); + void setPropertyStringMpv(const char* name, const char* value); + void setPropertyAsyncMpv(int value, MpvKey key); + void setPropertyAsyncMpv(const char* value, MpvKey key); + void setPropertyAsyncMpv(double value, MpvKey key); + void getPropertyAsyncMpv(MpvKey key); + + // Map to simulation time functions + double correctVideoPlaybackTime() const; + bool isWithingStartEndTime() const; + void updateFrameDuration(); + void stepFrameForward(); + void stepFrameBackward(); + + // Properties for user interaction + properties::TriggerProperty _play; + properties::TriggerProperty _pause; + properties::TriggerProperty _goToStart; + properties::TriggerProperty _reset; + properties::BoolProperty _playAudio; + + // Video properties. Try to read all these values from the video + std::string _videoFile; + double _currentVideoTime = 0.0; + double _fps = 24.0; // If when we read it it is 0, use 24 fps + double _videoDuration = 0.0; + glm::ivec2 _videoResolution = glm::ivec2(2048, 1024); // Used for the fbos + bool _isPaused = false; + PlaybackMode _playbackMode = PlaybackMode::RealTimeLoop; // Default is to loop + + // Maps for keeping track of libmpv commands and formats + std::map keys; + std::map formats; + + // Syncing with multiple nodes + double _correctPlaybackTime = 0.0; + double _deltaTime = 0.0; + + // Video stretching: map to simulation time animation mode + double _startJ200Time = 0.0; + double _endJ200Time = 0.0; + double _timeAtLastRender = 0.0; + double _frameDuration = 0.0; + + // Libmpv + mpv_handle* _mpvHandle = nullptr; + mpv_render_context* _mpvRenderContext = nullptr; + std::unique_ptr _frameTexture; + GLuint _fbo = 0; // Our opengl framebuffer where mpv renders to + int _wakeup = 0; // Signals when libmpv has a new frame ready + bool _isInitialized = false; // If libmpv has been inititalized + bool _isSeeking = false; // Prevent seeking while already seeking + bool _isDestroying = false; + double _seekThreshold = 1.0; // Threshold to ensure we seek to a different time +}; +} // namespace video::globebrowsing + +#endif // __OPENSPACE_MODULE_VIDEO___VIDEOPLAYER___H__ diff --git a/modules/video/include/videotileprovider.h b/modules/video/include/videotileprovider.h new file mode 100644 index 0000000000..739c112167 --- /dev/null +++ b/modules/video/include/videotileprovider.h @@ -0,0 +1,75 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_VIDEO___VIDEOTILEPROVIDER___H__ +#define __OPENSPACE_MODULE_VIDEO___VIDEOTILEPROVIDER___H__ + +#include + +#include +#include +#include +#include +#include + +// libmpv +#include +#include + +namespace openspace { struct Documentation; } + +namespace openspace { + +class VideoTileProvider : public globebrowsing::TileProvider { +public: + VideoTileProvider(const ghoul::Dictionary& dictionary); + ~VideoTileProvider(); + + void update() override final; + void reset() override final; + int minLevel() override final; + int maxLevel() override final; + float noDataValueAsFloat() override final; + globebrowsing::ChunkTile chunkTile(globebrowsing::TileIndex tileIndex, int parents, + int maxParents = 1337) override; + globebrowsing::Tile tile(const globebrowsing::TileIndex& tileIndex) override final; + globebrowsing::Tile::Status tileStatus( + const globebrowsing::TileIndex& tileIndex) override final; + globebrowsing::TileDepthTransform depthTransform() override final; + +private: + void internalInitialize() override final; + void internalDeinitialize() override final; + + // Tile handling + // Cache for rendering 1 frame + std::map _tileCache; + bool _tileIsReady = false; + + VideoPlayer _videoPlayer; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_VIDEO___VIDEOTILEPROVIDER___H__ diff --git a/modules/video/src/renderablevideoplane.cpp b/modules/video/src/renderablevideoplane.cpp new file mode 100644 index 0000000000..65052cb902 --- /dev/null +++ b/modules/video/src/renderablevideoplane.cpp @@ -0,0 +1,92 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include + +namespace openspace { + +RenderableVideoPlane::RenderableVideoPlane(const ghoul::Dictionary& dictionary) + : RenderablePlane(dictionary) + , _videoPlayer(dictionary) +{ + addPropertySubOwner(_videoPlayer); +} + +void RenderableVideoPlane::initializeGL() { + RenderablePlane::initializeGL(); + _videoPlayer.initialize(); +} + +void RenderableVideoPlane::deinitializeGL() { + _videoPlayer.destroy(); + RenderablePlane::deinitializeGL(); +} + +bool RenderableVideoPlane::isReady() const { + return RenderablePlane::isReady() && _videoPlayer.isInitialized(); +} + +void RenderableVideoPlane::render(const RenderData& data, RendererTasks& rendererTask) { + if (_videoPlayer.isInitialized()) { + RenderablePlane::render(data, rendererTask); + } +} + +void RenderableVideoPlane::update(const UpdateData& data) { + _videoPlayer.update(); + + if (!_videoPlayer.isInitialized()) { + return; + } + + // Shape the vidoe based on the aspect ration of the film + glm::vec2 textureDim = glm::vec2(_videoPlayer.frameTexture()->dimensions()); + if (_textureDimensions != textureDim) { + float aspectRatio = textureDim.x / textureDim.y; + float planeAspectRatio = _size.value().x / _size.value().y; + + if (std::abs(planeAspectRatio - aspectRatio) > + std::numeric_limits::epsilon()) + { + glm::vec2 newSize = + aspectRatio > 0.f ? + glm::vec2(_size.value().x * aspectRatio, _size.value().y) : + glm::vec2(_size.value().x, _size.value().y * aspectRatio); + _size = newSize; + } + + _textureDimensions = textureDim; + } + + RenderablePlane::update(data); +} + +void RenderableVideoPlane::bindTexture() { + _videoPlayer.frameTexture()->bind(); +} + +} // namespace openspace diff --git a/modules/video/src/renderablevideosphere.cpp b/modules/video/src/renderablevideosphere.cpp new file mode 100644 index 0000000000..2334c00755 --- /dev/null +++ b/modules/video/src/renderablevideosphere.cpp @@ -0,0 +1,392 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr std::array UniformNames = { + "opacity", "modelViewProjection", "modelViewRotation", "colorTexture", + "mirrorTexture" + }; + + enum class Orientation : int { + Outside = 0, + Inside, + Both + }; + + constexpr openspace::properties::Property::PropertyInfo MirrorTextureInfo = { + "MirrorTexture", + "Mirror Texture", + "Mirror the texture along the x-axis" + }; + + constexpr openspace::properties::Property::PropertyInfo OrientationInfo = { + "Orientation", + "Orientation", + "Specifies whether the texture is applied to the inside of the sphere, the " + "outside of the sphere, or both" + }; + + constexpr openspace::properties::Property::PropertyInfo SegmentsInfo = { + "Segments", + "Number of Segments", + "This value specifies the number of segments that the sphere is separated in" + }; + + constexpr openspace::properties::Property::PropertyInfo SizeInfo = { + "Size", + "Size (in meters)", + "This value specifies the radius of the sphere in meters" + }; + + constexpr openspace::properties::Property::PropertyInfo FadeOutThresholdInfo = { + "FadeOutThreshold", + "Fade-Out Threshold", + "This value determines percentage of the sphere is visible before starting " + "fading-out it" + }; + + constexpr openspace::properties::Property::PropertyInfo FadeInThresholdInfo = { + "FadeInThreshold", + "Fade-In Threshold", + "Distance from center of MilkyWay from where the astronomical object starts to " + "fade in" + }; + + constexpr openspace::properties::Property::PropertyInfo DisableFadeInOutInfo = { + "DisableFadeInOut", + "Disable Fade-In/Fade-Out effects", + "Enables/Disables the Fade-In/Out effects" + }; + + struct [[codegen::Dictionary(RenderableVideoSphere)]] Parameters { + // [[codegen::verbatim(SizeInfo.description)]] + float size; + + // [[codegen::verbatim(SegmentsInfo.description)]] + int segments; + + enum class [[codegen::map(Orientation)]] Orientation { + Outside, + Inside, + Both + }; + + // [[codegen::verbatim(OrientationInfo.description)]] + std::optional orientation; + + // [[codegen::verbatim(MirrorTextureInfo.description)]] + std::optional mirrorTexture; + + // [[codegen::verbatim(FadeOutThresholdInfo.description)]] + std::optional fadeOutThreshold [[codegen::inrange(0.0, 1.0)]]; + + // [[codegen::verbatim(FadeInThresholdInfo.description)]] + std::optional fadeInThreshold; + + // [[codegen::verbatim(DisableFadeInOutInfo.description)]] + std::optional disableFadeInOut; + }; +#include "renderablevideosphere_codegen.cpp" +} // namespace + +namespace openspace { + +documentation::Documentation RenderableVideoSphere::Documentation() { + return codegen::doc("renderable_video_sphere"); +} + +RenderableVideoSphere::RenderableVideoSphere(const ghoul::Dictionary& dictionary) + : Renderable(dictionary) + , _videoPlayer(dictionary) + , _orientation(OrientationInfo, properties::OptionProperty::DisplayType::Dropdown) + , _size(SizeInfo, 1.f, 0.f, 1e25f) + , _segments(SegmentsInfo, 8, 4, 1000) + , _mirrorTexture(MirrorTextureInfo, false) + , _disableFadeInDistance(DisableFadeInOutInfo, true) + , _fadeInThreshold(FadeInThresholdInfo, -1.f, 0.f, 1.f) + , _fadeOutThreshold(FadeOutThresholdInfo, -1.f, 0.f, 1.f) +{ + const Parameters p = codegen::bake(dictionary); + + addProperty(_opacity); + registerUpdateRenderBinFromOpacity(); + + _size = p.size; + _segments = p.segments; + + _orientation.addOptions({ + { static_cast(Orientation::Outside), "Outside" }, + { static_cast(Orientation::Inside), "Inside" }, + { static_cast(Orientation::Both), "Both" } + }); + + if (p.orientation.has_value()) { + _orientation = static_cast(codegen::map(*p.orientation)); + } + else { + _orientation = static_cast(Orientation::Outside); + } + addProperty(_orientation); + + _size.setExponent(15.f); + _size.onChange([this]() { + setBoundingSphere(_size); + _sphereIsDirty = true; + }); + addProperty(_size); + + addProperty(_segments); + _segments.onChange([this]() { _sphereIsDirty = true; }); + + addProperty(_mirrorTexture); + + _mirrorTexture = p.mirrorTexture.value_or(_mirrorTexture); + + bool hasGivenFadeOut = p.fadeOutThreshold.has_value(); + if (hasGivenFadeOut) { + _fadeOutThreshold = *p.fadeOutThreshold; + addProperty(_fadeOutThreshold); + } + + bool hasGivenFadeIn = p.fadeInThreshold.has_value(); + if (hasGivenFadeIn) { + _fadeInThreshold = *p.fadeInThreshold; + addProperty(_fadeInThreshold); + } + + if (hasGivenFadeIn || hasGivenFadeOut) { + _disableFadeInDistance = false; + addProperty(_disableFadeInDistance); + } + + setBoundingSphere(_size); + setRenderBinFromOpacity(); + addPropertySubOwner(_videoPlayer); +} + + +bool RenderableVideoSphere::isReady() const { + return _shader && _videoPlayer.isInitialized(); +} + +void RenderableVideoSphere::initializeGL() { + _sphere = std::make_unique(_size, _segments); + _sphere->initialize(); + + _shader = BaseModule::ProgramObjectManager.request( + "Sphere", + []() -> std::unique_ptr { + return global::renderEngine->buildRenderProgram( + "Sphere", + absPath("${MODULE_BASE}/shaders/sphere_vs.glsl"), + absPath("${MODULE_BASE}/shaders/sphere_fs.glsl") + ); + } + ); + + ghoul::opengl::updateUniformLocations(*_shader, _uniformCache, UniformNames); + + _videoPlayer.initialize(); +} + +void RenderableVideoSphere::deinitializeGL() { + _videoPlayer.destroy(); + BaseModule::ProgramObjectManager.release( + "Sphere", + [](ghoul::opengl::ProgramObject* p) { + global::renderEngine->removeRenderProgram(p); + } + ); + _shader = nullptr; +} + +void RenderableVideoSphere::render(const RenderData& data, RendererTasks&) { + Orientation orientation = static_cast(_orientation.value()); + + glm::dmat4 modelTransform = + glm::translate(glm::dmat4(1.0), data.modelTransform.translation) * + glm::dmat4(data.modelTransform.rotation) * + glm::scale(glm::dmat4(1.0), glm::dvec3(data.modelTransform.scale)); + + glm::dmat3 modelRotation = + glm::dmat3(data.modelTransform.rotation); + + // Activate shader + using IgnoreError = ghoul::opengl::ProgramObject::IgnoreError; + _shader->activate(); + _shader->setIgnoreUniformLocationError(IgnoreError::Yes); + + glm::mat4 modelViewProjection = data.camera.projectionMatrix() * + glm::mat4(data.camera.combinedViewMatrix() * modelTransform); + _shader->setUniform(_uniformCache.modelViewProjection, modelViewProjection); + + glm::mat3 modelViewRotation = glm::mat3( + glm::dmat3(data.camera.viewRotationMatrix()) * modelRotation + ); + _shader->setUniform(_uniformCache.modelViewRotation, modelViewRotation); + + float adjustedOpacity = opacity(); + + if (!_disableFadeInDistance) { + if (_fadeInThreshold > -1.0) { + const double d = glm::distance( + data.camera.positionVec3(), + data.modelTransform.translation + ); + const float logDist = + d > 0.0 ? + std::log(static_cast(d)) : + -std::numeric_limits::max(); + + const float startLogFadeDistance = glm::log(_size * _fadeInThreshold); + const float stopLogFadeDistance = startLogFadeDistance + 1.f; + + if (logDist > startLogFadeDistance && logDist < stopLogFadeDistance) { + const float fadeFactor = glm::clamp( + (logDist - startLogFadeDistance) / + (stopLogFadeDistance - startLogFadeDistance), + 0.f, + 1.f + ); + adjustedOpacity *= fadeFactor; + } + else if (logDist <= startLogFadeDistance) { + adjustedOpacity = 0.f; + } + } + + if (_fadeOutThreshold > -1.0) { + const double d = glm::distance( + data.camera.positionVec3(), + data.modelTransform.translation + ); + const float logDist = + d > 0.0 ? + std::log(static_cast(d)) : + -std::numeric_limits::max(); + const float startLogFadeDistance = glm::log(_size * _fadeOutThreshold); + const float stopLogFadeDistance = startLogFadeDistance + 1.f; + + if (logDist > startLogFadeDistance && logDist < stopLogFadeDistance) { + const float fadeFactor = glm::clamp( + (logDist - startLogFadeDistance) / + (stopLogFadeDistance - startLogFadeDistance), + 0.f, + 1.f + ); + adjustedOpacity *= (1.f - fadeFactor); + } + else if (logDist >= stopLogFadeDistance) { + adjustedOpacity = 0.f; + } + } + } + // Performance wise + if (adjustedOpacity < 0.01f) { + return; + } + + _shader->setUniform(_uniformCache.opacity, adjustedOpacity); + _shader->setUniform(_uniformCache._mirrorTexture, _mirrorTexture.value()); + + ghoul::opengl::TextureUnit unit; + unit.activate(); + bindTexture(); + defer{ unbindTexture(); }; + _shader->setUniform(_uniformCache.colorTexture, unit); + + // Setting these states should not be necessary, + // since they are the default state in OpenSpace. + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + + if (orientation == Orientation::Inside) { + glCullFace(GL_FRONT); + } + else if (orientation == Orientation::Both) { + glDisable(GL_CULL_FACE); + } + + if (_renderBin == Renderable::RenderBin::PreDeferredTransparent) { + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + glDepthMask(false); + } + + _sphere->render(); + + if (_renderBin == Renderable::RenderBin::PreDeferredTransparent) { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(true); + } + + _shader->setIgnoreUniformLocationError(IgnoreError::No); + _shader->deactivate(); + + if (orientation == Orientation::Inside) { + glCullFace(GL_BACK); + } + else if (orientation == Orientation::Both) { + glEnable(GL_CULL_FACE); + } +} + +void RenderableVideoSphere::update(const UpdateData&) { + _videoPlayer.update(); + + if (_shader->isDirty()) { + _shader->rebuildFromFile(); + ghoul::opengl::updateUniformLocations(*_shader, _uniformCache, UniformNames); + } + + if (_sphereIsDirty) { + _sphere = std::make_unique(_size, _segments); + _sphere->initialize(); + _sphereIsDirty = false; + } +} + +void RenderableVideoSphere::bindTexture() { + _videoPlayer.frameTexture().get()->bind(); +} + +void RenderableVideoSphere::unbindTexture() {} +} // namespace openspace diff --git a/modules/video/src/screenspacevideo.cpp b/modules/video/src/screenspacevideo.cpp new file mode 100644 index 0000000000..fbd80215e7 --- /dev/null +++ b/modules/video/src/screenspacevideo.cpp @@ -0,0 +1,93 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace openspace { + +ScreenSpaceVideo::ScreenSpaceVideo(const ghoul::Dictionary& dictionary) + : ScreenSpaceRenderable(dictionary) + , _videoPlayer(dictionary) +{ + // @TODO (abock, 2021-02-02) Should this be the name variable? The identifier wasn't + // declared in the documentation + std::string identifier; + if (dictionary.hasValue(KeyIdentifier)) { + identifier = dictionary.value(KeyIdentifier); + } + else { + identifier = "ScreenSpaceVideo"; + } + identifier = makeUniqueIdentifier(identifier); + setIdentifier(identifier); + + addPropertySubOwner(_videoPlayer); +} + +void ScreenSpaceVideo::update() { + _videoPlayer.update(); + + if (!_videoPlayer.isInitialized()) { + return; + } + glm::uvec3 texDimensions = _videoPlayer.frameTexture()->dimensions(); + if (_objectSize != glm::ivec2(texDimensions.x, texDimensions.y)) { + _objectSize = texDimensions; + } +} + +void ScreenSpaceVideo::render() { + if (_videoPlayer.isInitialized()) { + ScreenSpaceRenderable::render(); + } +} + +bool ScreenSpaceVideo::initializeGL() { + _videoPlayer.initialize(); + + return ScreenSpaceRenderable::initializeGL(); +} + +bool ScreenSpaceVideo::deinitializeGL() { + _videoPlayer.destroy(); + + return ScreenSpaceRenderable::deinitializeGL(); +} + +void ScreenSpaceVideo::bindTexture() { + _videoPlayer.frameTexture()->bind(); +} + +} // namespace openspace diff --git a/modules/video/src/videoplayer.cpp b/modules/video/src/videoplayer.cpp new file mode 100644 index 0000000000..89bf0745dc --- /dev/null +++ b/modules/video/src/videoplayer.cpp @@ -0,0 +1,922 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr std::string_view _loggerCat = "VideoPlayer"; + + constexpr openspace::properties::Property::PropertyInfo VideoInfo = { + "Video", + "Video", + "This should point to the video file that should be played." + }; + + constexpr openspace::properties::Property::PropertyInfo PlayInfo = { + "Play", + "Play", + "Play video" + }; + + constexpr openspace::properties::Property::PropertyInfo PauseInfo = { + "Pause", + "Pause", + "Pause video" + }; + + constexpr openspace::properties::Property::PropertyInfo GoToStartInfo = { + "GoToStart", + "Go To Start", + "Go to start in video" + }; + + constexpr openspace::properties::Property::PropertyInfo ResetInfo = { + "Reset", + "Reset", + "Reset video" + }; + + constexpr openspace::properties::Property::PropertyInfo AudioInfo = { + "PlayAudio", + "Play Audio", + "Play audio" + }; + + constexpr openspace::properties::Property::PropertyInfo StartTimeInfo = { + "StartTime", + "Start Time", + "The date and time that the video should start in the format " + "'YYYY MM DD hh:mm:ss'." + }; + + constexpr openspace::properties::Property::PropertyInfo EndTimeInfo = { + "EndTime", + "End Time", + "The date and time that the video should end in the format " + "'YYYY MM DD hh:mm:ss'." + }; + + constexpr openspace::properties::Property::PropertyInfo PlaybackModeInfo = { + "PlaybackMode", + "Playback Mode", + "Determines the way the video should be played. The start and end time of the " + "video can be set, or the video can be played as a loop in real time." + }; + + struct [[codegen::Dictionary(VideoPlayer)]] Parameters { + // [[codegen::verbatim(VideoInfo.description)]] + std::string video; + + // [[codegen::verbatim(AudioInfo.description)]] + std::optional playAudio; + + // [[codegen::verbatim(StartTimeInfo.description)]] + std::optional startTime [[codegen::datetime()]]; + + // [[codegen::verbatim(EndTimeInfo.description)]] + std::optional endTime [[codegen::datetime()]]; + + enum class PlaybackMode { + MapToSimulationTime = 0, + RealTimeLoop + }; + + // The mode of how the video should be played back. + // Default is video is played back according to the set start and end times. + std::optional playbackMode; + }; +#include "videoplayer_codegen.cpp" +} // namespace + +namespace openspace { + +namespace { + +bool checkMpvError(int status) { + if (status < 0) { + LERROR(fmt::format("Libmpv API error: {}", mpv_error_string(status))); + return false; + } + return true; +} + +void* getOpenGLProcAddress(void*, const char* name) { + return reinterpret_cast( + global::windowDelegate->openGLProcedureAddress(name) + ); +} +} // namespace + +void VideoPlayer::onMpvRenderUpdate(void* ctx) { + // The wakeup flag is set here to enable the mpv_render_context_render + // path in the main loop. + // The pattern here with a static function and a void pointer to the the class + // instance is a common pattern where C++ integrates a C library + static_cast(ctx)->_wakeup = 1; +} + +void VideoPlayer::observePropertyMpv(MpvKey key) { + mpv_observe_property( + _mpvHandle, + static_cast(key), + keys[key], + formats[key] + ); +} + +void VideoPlayer::setPropertyStringMpv(const char* name, const char* value) { + int result = mpv_set_property_string(_mpvHandle, name, value); + if (!checkMpvError(result)) { + LWARNING(fmt::format("Error setting property {}", name)); + } +} + +void VideoPlayer::setPropertyAsyncMpv(int value, MpvKey key) { + if (!_isInitialized) { + return; + } + int result = mpv_set_property_async( + _mpvHandle, + static_cast(key), + keys[key], + formats[key], + &value + ); + if (!checkMpvError(result)) { + LWARNING("Error when playing video"); + } +} + +void VideoPlayer::setPropertyAsyncMpv(const char* value, MpvKey key) { + if (!_isInitialized) { + return; + } + int result = mpv_set_property_async( + _mpvHandle, + static_cast(key), + keys[key], + formats[key], + &value + ); + if (!checkMpvError(result)) { + LWARNING("Error when playing video"); + } +} + +void VideoPlayer::setPropertyAsyncMpv(double value, MpvKey key) { + if (!_isInitialized) { + return; + } + int result = mpv_set_property_async( + _mpvHandle, + static_cast(key), + keys[key], + formats[key], + &value + ); + if (!checkMpvError(result)) { + LWARNING("Error when playing video"); + } +} + +void VideoPlayer::getPropertyAsyncMpv(MpvKey key) { + int result = mpv_get_property_async( + _mpvHandle, + static_cast(key), + keys[key], + formats[key] + ); + if (!checkMpvError(result)) { + LWARNING(fmt::format("Could not find property {}", keys[key])); + return; + } +} + +void VideoPlayer::commandAsyncMpv(const char* cmd[], MpvKey key) { + int result = mpv_command_async( + _mpvHandle, + static_cast(key), + cmd + ); + if (!checkMpvError(result)) { + LERROR(fmt::format("Could not execute command {}", keys[key])); + return; + } +} + +documentation::Documentation VideoPlayer::Documentation() { + return codegen::doc("video_videoplayer"); +} + +VideoPlayer::VideoPlayer(const ghoul::Dictionary& dictionary) + : PropertyOwner({ "VideoPlayer" }) + , _play(PlayInfo) + , _pause(PauseInfo) + , _goToStart(GoToStartInfo) + , _reset(ResetInfo) + , _playAudio(AudioInfo, false) +{ + ZoneScoped + + const Parameters p = codegen::bake(dictionary); + + _videoFile = p.video; + + _reset.onChange([this]() { reset(); }); + addProperty(_reset); + _playAudio.onChange([this]() { toggleMute(); }); + addProperty(_playAudio); + + if (_playbackMode == PlaybackMode::RealTimeLoop) { + // Video interaction. Only valid for real time looping + _play.onChange([this]() { play(); }); + addProperty(_play); + _pause.onChange([this]() { pause(); }); + addProperty(_pause); + _goToStart.onChange([this]() { goToStart(); }); + addProperty(_goToStart); + } + + if (p.playbackMode.has_value()) { + switch (*p.playbackMode) { + case Parameters::PlaybackMode::RealTimeLoop: + _playbackMode = PlaybackMode::RealTimeLoop; + break; + case Parameters::PlaybackMode::MapToSimulationTime: + _playbackMode = PlaybackMode::MapToSimulationTime; + break; + default: + LERROR("Missing playback mode in VideoTileProvider"); + throw ghoul::MissingCaseException(); + } + } + + if (_playbackMode == PlaybackMode::MapToSimulationTime) { + if (!p.startTime.has_value() || !p.endTime.has_value()) { + LERROR("Video tile layer tried to map to simulation time but lacked start or" + " end time" + ); + return; + } + _startJ200Time = Time::convertTime(*p.startTime); + _endJ200Time = Time::convertTime(*p.endTime); + ghoul_assert(_endJ200Time > _startJ200Time, "Invalid times for video"); + } + + global::syncEngine->addSyncable(this); + + keys = { + { MpvKey::Pause, "pause" }, + { MpvKey::Params, "video-params" }, + { MpvKey::Time, "time-pos" }, + { MpvKey::Duration, "duration" }, + { MpvKey::Height, "height" }, + { MpvKey::Width, "width" }, + { MpvKey::Meta, "metadata" }, + { MpvKey::Fps, "container-fps" }, + { MpvKey::IsSeeking, "seeking" }, + { MpvKey::Mute, "mute" }, + { MpvKey::Seek, "seek" } + }; + + formats = { + { MpvKey::Pause, MPV_FORMAT_FLAG }, + { MpvKey::Params, MPV_FORMAT_NODE }, + { MpvKey::Time, MPV_FORMAT_DOUBLE }, + { MpvKey::Duration, MPV_FORMAT_DOUBLE }, + { MpvKey::Height, MPV_FORMAT_INT64 }, + { MpvKey::Width, MPV_FORMAT_INT64 }, + { MpvKey::Meta, MPV_FORMAT_NODE }, + { MpvKey::Fps, MPV_FORMAT_DOUBLE }, + { MpvKey::IsSeeking, MPV_FORMAT_FLAG }, + { MpvKey::Mute, MPV_FORMAT_STRING } + }; +} + +VideoPlayer::~VideoPlayer() {} + +void VideoPlayer::pause() { + int isPaused = 1; + setPropertyAsyncMpv(isPaused, MpvKey::Pause); +} + +void VideoPlayer::play() { + int isPaused = 0; + setPropertyAsyncMpv(isPaused, MpvKey::Pause); +} + +void VideoPlayer::goToStart() { + seekToTime(0.0); +} + +void VideoPlayer::stepFrameForward() { + if (!_isInitialized) { + return; + } + const char* cmd[] = { "frame-step", nullptr }; + commandAsyncMpv(cmd); +} + +void VideoPlayer::stepFrameBackward() { + if (!_isInitialized) { + return; + } + const char* cmd[] = { "frame-back-step", nullptr }; + commandAsyncMpv(cmd); +} + +void VideoPlayer::initialize() { + initializeMpv(); +} + +void VideoPlayer::initializeMpv() { + _mpvHandle = mpv_create(); + if (!_mpvHandle) { + LINFO("LibMpv: mpv context init failed"); + } + + // Set libmpv flags before initializing + // See order at https://github.com/mpv-player/mpv/blob/master/libmpv/client.h#L420 + // Avoiding async calls in uninitialized state + + // Loop video + // https://mpv.io/manual/master/#options-loop + setPropertyStringMpv("loop", ""); + + // Allow only OpenGL (requires OpenGL 2.1+ or GLES 2.0+) + // https://mpv.io/manual/master/#options-gpu-api + setPropertyStringMpv("gpu-api", "opengl"); + + // Enable hardware decoding + // https://mpv.io/manual/master/#options-hwdec + setPropertyStringMpv("hwdec", "auto"); + + // Enable direct rendering (default: auto). If this is set to yes, the video will be + // decoded directly to GPU video memory (or staging buffers). + // https://mpv.io/manual/master/#options-vd-lavc-dr + setPropertyStringMpv("vd-lavc-dr", "yes"); + + // Print libmpv couts to the terminal + // https://mpv.io/manual/master/#options-terminal + setPropertyStringMpv("terminal", "yes"); + + // Control how long before video display target time the frame should be rendered + // https://mpv.io/manual/master/#options-video-timing-offset + setPropertyStringMpv("video-timing-offset", "0"); + + // Turn off audio as default + setPropertyStringMpv("mute", "yes"); + + // Starting MPV in a paused state seems to reduce problems with initialization + setPropertyStringMpv("pause", ""); + + // Verbose mode for debug purposes + // setPropertyStringMpv("msg-level", "all=v"); + // mpv_request_log_messages(_mpvHandle, "debug"); + + if (mpv_initialize(_mpvHandle) < 0) { + LINFO("mpv init failed"); + } + + mpv_opengl_init_params gl_init_params{ getOpenGLProcAddress, nullptr }; + int adv = 1; // Use libmpv advanced mode since we will use the update callback + // Decouple mpv from waiting to get the correct fps. Use with flag video-timing-offset + // set to 0 + int blockTime = 0; + + mpv_render_param params[]{ + { MPV_RENDER_PARAM_API_TYPE, const_cast(MPV_RENDER_API_TYPE_OPENGL) }, + { MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params }, + { MPV_RENDER_PARAM_ADVANCED_CONTROL, &adv }, + { MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, &blockTime }, + { MPV_RENDER_PARAM_INVALID, nullptr } + }; + + // This makes mpv use the currently set GL context. It will use the callback + // (passed via params) to resolve GL builtin functions, as well as extensions. + int result = mpv_render_context_create(&_mpvRenderContext, _mpvHandle, params); + if (result < 0) { + LINFO("Failed to initialize libmpv OpenGL context"); + } + + // When there is a need to call mpv_render_context_update(), which can + // request a new frame to be rendered. + // (Separate from the normal event handling mechanism for the sake of + // users which run OpenGL on a different thread.) + mpv_render_context_set_update_callback( + _mpvRenderContext, + onMpvRenderUpdate, + this + ); + + // Load file + std::string file = _videoFile; + const char* cmd[] = { "loadfile", file.c_str(), nullptr }; + result = mpv_command(_mpvHandle, cmd); + if (!checkMpvError(result)) { + LERROR("Could not open video file"); + return; + } + + //Create FBO to render video into + createFBO(_videoResolution.x, _videoResolution.y); + + //Observe video parameters + observePropertyMpv(MpvKey::Params); + observePropertyMpv(MpvKey::Duration); + observePropertyMpv(MpvKey::Meta); + observePropertyMpv(MpvKey::Height); + observePropertyMpv(MpvKey::Width); + observePropertyMpv(MpvKey::Pause); + observePropertyMpv(MpvKey::Fps); + observePropertyMpv(MpvKey::Time); + observePropertyMpv(MpvKey::IsSeeking); + + _isInitialized = true; +} + +void VideoPlayer::seekToTime(double time, PauseAfterSeek pauseAfter) { + if (_isSeeking || abs(_currentVideoTime - time) < glm::epsilon()) { + return; + } + pause(); + setPropertyAsyncMpv(time, MpvKey::Time); + if (!pauseAfter) { + play(); + } +} + +void VideoPlayer::toggleMute() { + const char* mute = _playAudio ? "no" : "yes"; + setPropertyAsyncMpv(mute, MpvKey::Mute); +} + +void VideoPlayer::update() { + if (_isDestroying) { + return; + } + if (_playbackMode == PlaybackMode::MapToSimulationTime) { + seekToTime(correctVideoPlaybackTime()); + } + if (_mpvRenderContext && _mpvHandle) { + renderMpv(); + } +} + +void VideoPlayer::renderMpv() { + handleMpvEvents(); + + if (_wakeup) { + uint64_t result = mpv_render_context_update(_mpvRenderContext); + if ((result & MPV_RENDER_UPDATE_FRAME)) { + // Save the currently bound fbo + GLint defaultFBO = ghoul::opengl::FramebufferObject::getActiveObject(); + + // See render_gl.h on what OpenGL environment mpv expects, and other API + // details. This function fills the fbo and texture with data, after it + // we can get the data on the GPU, not the CPU + int fboInt = static_cast(_fbo); + mpv_opengl_fbo mpfbo{ + fboInt, + _videoResolution.x, + _videoResolution.y, + 0 + }; + int flipY{ 1 }; + + mpv_render_param params[] = { + { MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo }, + { MPV_RENDER_PARAM_FLIP_Y, &flipY }, + { MPV_RENDER_PARAM_INVALID, nullptr } + }; + // This "renders" to the video_framebuffer "linked by ID" in the + // params_fbo + mpv_render_context_render(_mpvRenderContext, params); + + /* TODO: remove this comment in case we never encounter this issue again */ + // We have to set the Viewport on every cycle because + // mpv_render_context_render internally rescales the fb of the context(?!)... + global::renderEngine->openglStateCache().resetViewportState(); + + // We also need to reset the render target + glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); + + _wakeup = 0; + } + } +} + +void VideoPlayer::handleMpvEvents() { + while (_mpvHandle) { + mpv_event* event = mpv_wait_event(_mpvHandle, 0.0); + + // Validate event + if (event->event_id == MPV_EVENT_NONE) { + break; + } + if (!checkMpvError(event->error)) { + LWARNING(fmt::format( + "Error at mpv event : {} {}", event->event_id, event->reply_userdata + )); + break; + } + + switch (event->event_id) { + case MPV_EVENT_VIDEO_RECONFIG: { + // Retrieve the new video size + getPropertyAsyncMpv(MpvKey::Width); + getPropertyAsyncMpv(MpvKey::Height); + break; + } + case MPV_EVENT_PROPERTY_CHANGE: { + mpv_event_property* prop = + reinterpret_cast(event->data); + // Validate reply + if (prop->format == MPV_FORMAT_NONE) { + break; + } + // Validate reply with what we have stored + MpvKey key = static_cast(event->reply_userdata); + if (formats[key] != prop->format) { + LINFO(fmt::format("Wrong format for property {}", keys[key])); + break; + } + getPropertyAsyncMpv(key); + break; + } + case MPV_EVENT_GET_PROPERTY_REPLY: { + handleMpvProperties(event); + break; + } + case MPV_EVENT_LOG_MESSAGE: { + mpv_event_log_message* msg = + reinterpret_cast(event->data); + std::stringstream ss; + LINFO(fmt::format("[{}] {}: {}", msg->prefix, msg->level, msg->text)); + break; + } + default: { + // Ignore uninteresting or unknown events. + break; + } + } + } +} + +void VideoPlayer::handleMpvProperties(mpv_event* event) { + MpvKey key = static_cast(event->reply_userdata); + + if (!event->data) { + LERROR(fmt::format("Could not find data for property: {}", keys[key])); + return; + } + // Cast event to node or property depending on its format + mpv_event_property* prop = nullptr; + mpv_node node; + if (formats[key] == MPV_FORMAT_NODE) { + int result = mpv_event_to_node(&node, event); + if (!checkMpvError(result)) { + LWARNING( + fmt::format("Error getting data from libmpv property {}", keys[key]) + ); + } + } + else { + prop = reinterpret_cast(event->data); + } + + // Handle new values + switch (key) { + case MpvKey::Duration: { + double* duration = reinterpret_cast(prop->data); + + if (!duration) { + LERROR("Could not find duration property"); + break; + } + + _videoDuration = *duration; + if (_playbackMode == PlaybackMode::MapToSimulationTime) { + updateFrameDuration(); + } + + LINFO(fmt::format("Duration: {}", *duration)); + break; + } + case MpvKey::Height: { + int* height = reinterpret_cast(prop->data); + + if (!height) { + LERROR("Could not find height property"); + break; + } + + if (*height == _videoResolution.y) { + break; + } + + LINFO(fmt::format("New height: {}", *height)); + + if (*height > 0 && _videoResolution.x > 0 && _fbo > 0) { + resizeFBO(_videoResolution.x, *height); + } + + break; + } + case MpvKey::Width: { + int* width = reinterpret_cast(prop->data); + + if (!width) { + LERROR("Could not find width property"); + break; + } + + if (*width == _videoResolution.y) { + break; + } + + LINFO(fmt::format("New width: {}", *width)); + + if (*width > 0 && _videoResolution.y > 0 && _fbo > 0) { + resizeFBO(*width, _videoResolution.y); + } + + break; + } + case MpvKey::Time: { + double* time = reinterpret_cast(prop->data); + + if (!time) { + LERROR("Could not find playback time property"); + break; + } + _currentVideoTime = *time; + break; + } + case MpvKey::IsSeeking: { + bool* isSeekingBool = reinterpret_cast(prop->data); + _isSeeking = *isSeekingBool; + break; + } + case MpvKey::Fps: { + double* fps = reinterpret_cast(prop->data); + if (*fps < glm::epsilon()) { + _fps = 24.0; + LWARNING("Detected fps was 0. Falling back on 24 fps"); + break; + } + if (!fps) { + LERROR("Could not find fps property"); + break; + } + _fps = *fps; + if (_playbackMode == PlaybackMode::MapToSimulationTime) { + updateFrameDuration(); + } + + LINFO(fmt::format("Detected fps: {}", *fps)); + _seekThreshold = 2.0 * (1.0 / _fps); + break; + } + case MpvKey::Pause: { + int* videoIsPaused = reinterpret_cast(prop->data); + _isPaused = (* videoIsPaused == 1); + break; + } + case MpvKey::Meta: { + LINFO("Printing meta data reply"); + if (node.format == MPV_FORMAT_NODE_MAP) { + for (int n = 0; n < node.u.list->num; n++) { + if (node.u.list->values[n].format == MPV_FORMAT_STRING) { + LINFO(node.u.list->values[n].u.string); + } + } + } + else { + LWARNING("No meta data could be read"); + } + + break; + } + case MpvKey::Params: { + if (node.format == MPV_FORMAT_NODE_ARRAY || + node.format == MPV_FORMAT_NODE_MAP) + { + mpv_node_list* list = node.u.list; + + mpv_node width, height; + bool foundWidth = false; + bool foundHeight = false; + for (int i = 0; i < list->num; ++i) { + if (foundWidth && foundHeight) { + break; + } + + if (list->keys[i] == "w") { + width = list->values[i]; + foundWidth = true; + } + else if (list->keys[i] == "h") { + height = list->values[i]; + foundHeight = true; + } + } + + if (!foundWidth || !foundHeight) { + LINFO("Could not find width or height params from parameters"); + return; + } + + int w = -1; + int h = -1; + if (width.format == MPV_FORMAT_INT64) { + w = width.u.int64; + } + if (height.format == MPV_FORMAT_INT64) { + h = height.u.int64; + } + + if (w == -1 || h == -1) { + LERROR("Invalid width or height params"); + return; + } + resizeFBO(w, h); + } + break; + } + default: { + throw ghoul::MissingCaseException(); + } + } +} + +void VideoPlayer::destroy() { + _isDestroying = true; + // Destroy the GL renderer and all of the GL objects it allocated. If video + // is still running, the video track will be deselected. + mpv_render_context_free(_mpvRenderContext); + _mpvRenderContext = nullptr; + mpv_destroy(_mpvHandle); + _mpvHandle = nullptr; + glDeleteFramebuffers(1, &_fbo); +} + +void VideoPlayer::preSync(bool isMaster) { + _correctPlaybackTime = isMaster ? _currentVideoTime : -1.0; +} + +void VideoPlayer::encode(SyncBuffer* syncBuffer) { + syncBuffer->encode(_correctPlaybackTime); +} + +void VideoPlayer::decode(SyncBuffer* syncBuffer) { + syncBuffer->decode(_correctPlaybackTime); +} + +void VideoPlayer::postSync(bool isMaster) { + if (_correctPlaybackTime < 0.0) { + return; + } + // Ensure the nodes have the same time as the master node + bool isMappingTime = _playbackMode == PlaybackMode::MapToSimulationTime; + if (!isMaster) { + if ((_correctPlaybackTime - _currentVideoTime) > glm::epsilon()) { + seekToTime(_correctPlaybackTime, PauseAfterSeek(isMappingTime)); + } + } +} + +const std::unique_ptr& VideoPlayer::frameTexture() const { + return _frameTexture; +} + +void VideoPlayer::reset() { + if (_videoFile.empty()) { + return; + } + destroy(); + _isDestroying = false; + initializeMpv(); +} + +bool VideoPlayer::isInitialized() const { + return _isInitialized; +} + +bool VideoPlayer::isWithingStartEndTime() const { + const double now = global::timeManager->time().j2000Seconds(); + return now <= _endJ200Time && now >= _startJ200Time; +} + +void VideoPlayer::updateFrameDuration() { + double openspaceVideoLength = (_endJ200Time - _startJ200Time) / _videoDuration; + _frameDuration = (1.0 / _fps) * openspaceVideoLength; +} + +double VideoPlayer::correctVideoPlaybackTime() const { + const double now = global::timeManager->time().j2000Seconds(); + double percentage = 0.0; + if (now > _endJ200Time) { + percentage = 1.0; + } + else if (now < _startJ200Time) { + percentage = 0.0; + } + else { + percentage = (now - _startJ200Time) / (_endJ200Time - _startJ200Time); + } + return percentage * _videoDuration; +} + +void VideoPlayer::createFBO(int width, int height) { + LINFO(fmt::format("Creating new FBO with width: {} and height: {}", width, height)); + + if (width <= 0 || height <= 0) { + LERROR("Cannot create empty fbo"); + return; + } + + // Update resolution of video + _videoResolution = glm::ivec2(width, height); + + glGenFramebuffers(1, &_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, _fbo); + + _frameTexture = std::make_unique( + glm::uvec3(width, height, 1), + GL_TEXTURE_2D + ); + _frameTexture->uploadTexture(); + + // Configure + _frameTexture->bind(); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + // Disable mipmaps + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + + // Bind texture to framebuffer + glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, + *_frameTexture, + 0 + ); + + // Unbind FBO + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void VideoPlayer::resizeFBO(int width, int height) { + if (width == _videoResolution.x && height == _videoResolution.y) { + return; + } + LINFO(fmt::format("Resizing FBO with width: {} and height: {}", width, height)); + + // Update resolution of video + _videoResolution = glm::ivec2(width, height); + + // Delete old FBO and texture + glDeleteFramebuffers(1, &_fbo); + _frameTexture.reset(nullptr); + + createFBO(width, height); +} + +} // namespace openspace::video diff --git a/modules/video/src/videotileprovider.cpp b/modules/video/src/videotileprovider.cpp new file mode 100644 index 0000000000..f1de10859b --- /dev/null +++ b/modules/video/src/videotileprovider.cpp @@ -0,0 +1,156 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr std::string_view _loggerCat = "VideoTileProvider"; +} // namespace + +namespace openspace { + +namespace { + +bool isDifferent(double first, double second) { + return abs(first - second) > glm::epsilon(); +} +} + +VideoTileProvider::VideoTileProvider(const ghoul::Dictionary& dictionary) + : _videoPlayer(dictionary) +{ + ZoneScoped + addPropertySubOwner(_videoPlayer); +} + +VideoTileProvider::~VideoTileProvider() {} + +globebrowsing::Tile VideoTileProvider::tile(const globebrowsing::TileIndex& tileIndex) { + ZoneScoped + + if (!_videoPlayer.isInitialized()) { + return globebrowsing::Tile(); + } + + // Always check that our framebuffer is ok + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + LINFO("Framebuffer is not complete"); + } + + // For now, don't use the cache as we're trying to debug the problem w playback + uint64_t hash = tileIndex.hashKey(); + auto foundTile = _tileCache.find(hash); + bool textureChanged = foundTile != _tileCache.end() && + foundTile->second.texture != _videoPlayer.frameTexture().get(); + + if (foundTile == _tileCache.end() || textureChanged) { + _tileCache[hash] = globebrowsing::Tile{ + _videoPlayer.frameTexture().get(), + std::nullopt, + globebrowsing::Tile::Status::OK + }; + } + return _tileCache[hash]; +} + +globebrowsing::Tile::Status VideoTileProvider::tileStatus(const globebrowsing::TileIndex& tileIndex) { + if (tileIndex.level > maxLevel()) { + return globebrowsing::Tile::Status::OutOfRange; + } + else if (_tileIsReady) { + return globebrowsing::Tile::Status::OK; + } + else { + return globebrowsing::Tile::Status::Unavailable; + } +} + +globebrowsing::TileDepthTransform VideoTileProvider::depthTransform() { + return { 0.f, 1.f }; +} + +void VideoTileProvider::update() { + _videoPlayer.update(); +} + +void VideoTileProvider::reset() { + _videoPlayer.reset(); +} + +globebrowsing::ChunkTile VideoTileProvider::chunkTile(globebrowsing::TileIndex tileIndex, + int parents, int maxParents) { + using namespace globebrowsing; + std::function ascendToParent = [] + (TileIndex& ti, TileUvTransform& uv) { + ti.level--; + }; + + glm::vec2 noOfTiles = { + std::pow(2, tileIndex.level), + std::pow(2, tileIndex.level - 1) + }; + glm::vec2 ratios = { 1.f / noOfTiles.x, 1.f / noOfTiles.y }; + float offsetX = ratios.x * static_cast(tileIndex.x); + // The tiles on the y-axis should be traversed backwards + float offsetY = ratios.y * (noOfTiles.y - static_cast(tileIndex.y) - 1.f); + + TileUvTransform uvTransform = { glm::vec2(offsetX, offsetY), ratios }; + + return traverseTree(tileIndex, parents, maxParents, ascendToParent, uvTransform); +} + +int VideoTileProvider::minLevel() { + return 1; +} + +int VideoTileProvider::maxLevel() { + // This is the level where above the tile is marked as unavailable and is no longer + // displayed. Since we want to display the tiles at all times we set the max level + return 1337; +} + +float VideoTileProvider::noDataValueAsFloat() { + return std::numeric_limits::min(); +} + +void VideoTileProvider::internalInitialize() { + _videoPlayer.initialize(); +} + +void VideoTileProvider::internalDeinitialize() { + _videoPlayer.destroy(); +} + +} // namespace openspace diff --git a/modules/video/videomodule.cpp b/modules/video/videomodule.cpp new file mode 100644 index 0000000000..aeb54574e4 --- /dev/null +++ b/modules/video/videomodule.cpp @@ -0,0 +1,87 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr openspace::properties::Property::PropertyInfo EnabledInfo = { + "Enabled", + "Enabled", + "Decides if this module should be enabled" + }; + + struct [[codegen::Dictionary(VideoModule)]] Parameters { + // [[codegen::verbatim(EnabledInfo.description)]] + std::optional enabled; + }; + +#include "videomodule_codegen.cpp" +} // namespace + +namespace openspace { + +VideoModule::VideoModule() + : OpenSpaceModule(VideoModule::Name) + , _enabled(EnabledInfo) +{ + addProperty(_enabled); +} + +void VideoModule::internalInitialize(const ghoul::Dictionary& dict) { + const Parameters p = codegen::bake(dict); + + _enabled = p.enabled.value_or(_enabled); + + ghoul::TemplateFactory* fTileProvider = + FactoryManager::ref().factory(); + ghoul_assert(fTileProvider, "TileProvider factory was not created"); + fTileProvider->registerClass("VideoTileLayer"); + + ghoul::TemplateFactory* fSsRenderable = + FactoryManager::ref().factory(); + ghoul_assert(fSsRenderable, "ScreenSpaceRenderable factory was not created"); + + fSsRenderable->registerClass("ScreenSpaceVideo"); + + ghoul::TemplateFactory* fRenderable = + FactoryManager::ref().factory(); + ghoul_assert(fRenderable, "Renderable factory was not created"); + fRenderable->registerClass("RenderableVideoSphere"); + fRenderable->registerClass("RenderableVideoPlane"); +} + +std::vector VideoModule::documentations() const { + return std::vector(); +} + +} // namespace openspace diff --git a/modules/video/videomodule.h b/modules/video/videomodule.h new file mode 100644 index 0000000000..20d89290db --- /dev/null +++ b/modules/video/videomodule.h @@ -0,0 +1,51 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_VIDEO___VIDEOMODULE___H__ +#define __OPENSPACE_MODULE_VIDEO___VIDEOMODULE___H__ + +#include + +#include + +namespace openspace { + +class VideoModule : public OpenSpaceModule { +public: + constexpr static const char* Name = "Video"; + + VideoModule(); + + std::vector documentations() const override; + +protected: + void internalInitialize(const ghoul::Dictionary& dict) override; + +private: + properties::BoolProperty _enabled; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_VIDEO___VIDEOMODULE___H__