FlightGear next
splash.cxx
Go to the documentation of this file.
1/*
2 * SPDX-FileName: splash.cxx
3 * SPDX-FileComment: draws the initial splash screen. Written by Curtis Olson, started July 1998.
4 * SPDX-FileCopyrightText: Copyright (C) 1997 Michele F. America - nomimarketing@mail.telepac.pt
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8#include <config.h>
9
10#include <osg/BlendFunc>
11#include <osg/Camera>
12#include <osg/Depth>
13#include <osg/Geometry>
14#include <osg/Node>
15#include <osg/NodeCallback>
16#include <osg/NodeVisitor>
17#include <osg/StateSet>
18#include <osg/Switch>
19#include <osg/Texture2D>
20#include <osg/TextureRectangle>
21#include <osg/Version>
22
23#include <osgText/Text>
24#include <osgText/String>
25#include <osgDB/ReadFile>
26
27#include <simgear/compiler.h>
28
29#include <simgear/debug/logstream.hxx>
30#include <simgear/math/sg_random.hxx>
31#include <simgear/misc/sg_path.hxx>
32#include <simgear/misc/sg_dir.hxx>
33#include <simgear/scene/util/SGReaderWriterOptions.hxx>
34#include <simgear/scene/util/OsgUtils.hxx>
35#include <simgear/props/condition.hxx>
36
37#include "VRManager.hxx"
38#include "renderer.hxx"
39#include "splash.hxx"
40#include <Main/fg_os.hxx>
41#include <Main/fg_props.hxx>
42#include <Main/globals.hxx>
43#include <Main/locale.hxx>
44#include <Main/util.hxx>
46
47#include <sstream>
48
49
50static const char* LICENSE_URL_TEXT = "Licensed under the GNU GPL. See https://www.flightgear.org for more information";
51
52using namespace std::string_literals;
53using namespace simgear;
54
55class SplashScreenUpdateCallback : public osg::NodeCallback {
56public:
57 virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
58 {
59 SplashScreen* screen = static_cast<SplashScreen*>(node);
60 screen->doUpdate();
61 traverse(node, nv);
62 }
63};
64
66 _splashAlphaNode(fgGetNode("/sim/startup/splash-alpha", true))
67{
68#ifdef ENABLE_OSGXR
69 uint32_t splashW = 1920, splashH = 1080;
70 float aspect = (float)splashW / splashH;
71 _splashSwapchain = new osgXR::Swapchain(splashW, splashH);
72 _splashSwapchain->setAlphaBits(8);
73 _splashSwapchain->allowRGBEncoding(osgXR::Swapchain::Encoding::ENCODING_SRGB);
74 _splashLayer = new osgXR::CompositionLayerQuad(flightgear::VRManager::instance());
75 _splashLayer->setSubImage(_splashSwapchain);
76 _splashLayer->setSize(osg::Vec2f(aspect, 1.0f));
77 _splashLayer->setPosition(osg::Vec3f(0, 0, -2.0f));
78 _splashLayer->setAlphaMode(osgXR::CompositionLayer::BLEND_ALPHA_UNPREMULT);
79#endif
80
81 const std::string vertex_source =
82 "#version 330 core\n"
83 "\n"
84 "layout(location = 0) in vec4 pos;\n"
85 "layout(location = 3) in vec4 multitexcoord0;\n"
86 "\n"
87 "out vec2 texcoord;\n"
88 "\n"
89 "uniform mat4 osg_ModelViewProjectionMatrix;\n"
90 "\n"
91 "void main()\n"
92 "{\n"
93 " gl_Position = osg_ModelViewProjectionMatrix * pos;\n"
94 " texcoord = multitexcoord0.st;\n"
95 "}\n";
96 osg::Shader* vertex_shader = new osg::Shader(osg::Shader::VERTEX, vertex_source);
97
98 const std::string fragment_source =
99 "#version 330 core\n"
100 "\n"
101 "layout(location = 0) out vec4 fragColor;\n"
102 "\n"
103 "in vec2 texcoord;\n"
104 "\n"
105 "uniform sampler2D tex;\n"
106 "\n"
107 "void main()\n"
108 "{\n"
109 " fragColor = texture(tex, texcoord);\n"
110 "}\n";
111 osg::Shader* fragment_shader = new osg::Shader(osg::Shader::FRAGMENT, fragment_source);
112
113 _program = new osg::Program();
114 _program->addShader(vertex_shader);
115 _program->addShader(fragment_shader);
116
117 setName("splashGroup");
118 setUpdateCallback(new SplashScreenUpdateCallback);
119}
120
124
125void SplashScreen::createNodes()
126{
127 // The splash FBO is rendered as sRGB, but for VR it needs to be in an sRGB
128 // pixel format for it to be handled as such by OpenXR.
129 bool useSRGB = false;
131#if !defined(SG_MAC)
132 // SRGB does detect on macOS, but doesn't actually work, so we
133 // disable the check there.
134 if (guiCamera) {
135 osg::GraphicsContext* gc = guiCamera->getGraphicsContext();
136 osg::GLExtensions* glext = gc->getState()->get<osg::GLExtensions>();
137 if (glext) {
138 useSRGB = osg::isGLExtensionOrVersionSupported(glext->contextID, "GL_EXT_texture_sRGB", 2.1f) &&
139 osg::isGLExtensionOrVersionSupported(glext->contextID, "GL_EXT_framebuffer_sRGB", 3.0f);
140 }
141 }
142#endif
143 // setup the base geometry
144 _splashFBOTexture = new osg::Texture2D;
145 _splashFBOTexture->setInternalFormat(useSRGB ? GL_SRGB8 : GL_RGB);
146
147 _splashFBOTexture->setResizeNonPowerOfTwoHint(false);
148 _splashFBOTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR);
149 _splashFBOTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);
150
151 _splashFBOCamera = createFBOCamera();
152 addChild(_splashFBOCamera);
153
154 osg::Geometry* geometry = new osg::Geometry;
155 geometry->setSupportsDisplayList(false);
156
157 osg::Geode* geode = new osg::Geode;
158 _splashFBOCamera->addChild(geode);
159 geode->addDrawable(geometry);
160
161 // get localized GPL licence text to be displayed at splash screen startup
162 std::string licenseUrlText = globals->get_locale()->getLocalizedString("license-url", "sys", LICENSE_URL_TEXT);
163 // add the splash image
164 std::string splashImageName = selectSplashImage();
165 addImage(splashImageName, true, 0, 0, 1, 1, nullptr, true);
166
167 // parse the content from the tree
168 // there can be many <content> <model-content> and <image> nodes
169 // <content> is reserved for use in defaults.xml and is the basic
170 // text; model-content and images are for use in the model
171 // to present model related information.
172 auto root = globals->get_props()->getNode("/sim/startup");
173 bool legacySplashLogoMode = false;
174
175 // firstly add all image nodes.
176 std::vector<SGPropertyNode_ptr> images = root->getChildren("image");
177 if (!images.empty()) {
178 for (const auto& image : images) {
179 addImage(image->getStringValue("path", ""),
180 false,
181 image->getDoubleValue("x", 0.025f),
182 image->getDoubleValue("y", 0.935f),
183 image->getDoubleValue("width", 0.1),
184 image->getDoubleValue("height", 0.1),
185 image->getNode("condition"),
186 false);
187 }
188 } else {
189 // if there are no image nodes then revert to the legacy (2020.3 or before) way of doing things
190 auto splashLogoImage = fgGetString("/sim/startup/splash-logo-image");
191 if (!splashLogoImage.empty())
192 {
193 float logoX = fgGetDouble("/sim/startup/splash-logo-x-norm", 0.0);
194 float logoY = 1.0 - fgGetDouble("/sim/startup/splash-logo-y-norm", 0.065);
195
196 float logoWidth = fgGetDouble("/sim/startup/splash-logo-width", 0.6);
197
198 auto img = addImage(splashLogoImage, false, logoX, logoY, logoWidth, 0, nullptr, false);
199 if (img != nullptr)
200 legacySplashLogoMode = true;
201 }
202 }
203
204 // put this information into the property tree so the defaults or model can pull it in.
205 fgSetString("/sim/startup/splash-authors", flightgear::getAircraftAuthorsText());
206 fgSetString("/sim/startup/description", fgGetString("/sim/description"));
207 fgSetString("/sim/startup/title", "FlightGear "s + fgGetString("/sim/version/flightgear"));
208
209 if (!strcmp(FG_BUILD_TYPE, "Nightly")) {
210 fgSetString("sim/build-warning", globals->get_locale()->getLocalizedString("unstable-warning", "sys", "unstable!"));
211 fgSetBool("sim/build-warning-active", true);
212 }
213
214 // the licence node serves a dual purpose; both the licence URL and then after a delay
215 // (currently 5 seconds) it will display helpful information.
216 fgSetString("/sim/startup/licence", licenseUrlText);
217 fgSetString("/sim/startup/tip", licenseUrlText);
218
219#ifndef NDEBUG
220 fgSetBool("/sim/startup/build-type-debug", true);
221#else
222 fgSetBool("/sim/startup/build-type-debug", false);
223#endif
224
225 fgSetBool("/sim/startup/legacy-splash-screen", _legacySplashScreenMode);
226 fgSetBool("/sim/startup/legacy-splash-logo", legacySplashLogoMode);
227
228 // load all model content first.
229 for (const auto& content : root->getChildren("model-content")) {
230 CreateTextFromNode(content, geode, true);
231 }
232
233 // default content comes in second; and has the ability to be overriden by the model
234 for (const auto& content : root->getChildren("content")) {
235 if (content->getIndex()) { // Skip 0 element - reserved for future usage.
236 // default content can be hidden by the model. By hidden it will never be
237 // added (there is also the possibility to use a condition to dynamically hide content)
238 if (!content->getBoolValue("hide"))
239 CreateTextFromNode(content, geode, false);
240 }
241 }
242 // add main title last so it is atop all.
243 addText(geode, osg::Vec2(0.025f, 0.02f), 0.08, "FlightGear "s + fgGetString("/sim/version/flightgear"), osgText::Text::LEFT_TOP);
244
246
247 geometry = new osg::Geometry;
248 geometry->setSupportsDisplayList(false);
249
250 _splashSpinnerVertexArray = new osg::Vec3Array;
251 for (int i = 0; i < 12; ++i) {
252 _splashSpinnerVertexArray->push_back(osg::Vec3(0.0f, 0.0f, -0.1f));
253 }
254 geometry->setVertexArray(_splashSpinnerVertexArray);
255
256 // QColor buttonColor(27, 122, 211);
257 osg::Vec4Array* colorArray = new osg::Vec4Array;
258 colorArray->push_back(osg::Vec4(27 / 255.0f, 122 / 255.0f, 211 / 255.0f, 0.75f));
259 geometry->setColorArray(colorArray);
260 geometry->setColorBinding(osg::Geometry::BIND_OVERALL);
261 geometry->addPrimitiveSet(new osg::DrawArrays(GL_TRIANGLES, 0, 12));
262
263 geode->addDrawable(geometry);
264
266
267 _splashQuadCamera = new osg::Camera;
268 _splashQuadCamera->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
269 _splashQuadCamera->setViewMatrix(osg::Matrix::identity());
270 _splashQuadCamera->setProjectionMatrixAsOrtho2D(0.0, 1.0, 0.0, 1.0);
271 _splashQuadCamera->setAllowEventFocus(false);
272 _splashQuadCamera->setCullingActive(false);
273 _splashQuadCamera->setRenderOrder(osg::Camera::NESTED_RENDER);
274
275 osg::StateSet* stateSet = _splashQuadCamera->getOrCreateStateSet();
276 stateSet->setMode(GL_BLEND, osg::StateAttribute::ON);
277 stateSet->setAttribute(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA), osg::StateAttribute::ON);
278 stateSet->setRenderBinDetails(1000, "RenderBin");
279 stateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);
280
281 bool use_vertex_attribute_aliasing = false;
282 if (guiCamera) {
283 auto gc = guiCamera->getGraphicsContext();
284 use_vertex_attribute_aliasing = gc->getState()->getUseVertexAttributeAliasing();
285 }
286 if (use_vertex_attribute_aliasing) {
287 stateSet->setAttribute(_program);
288 stateSet->addUniform(new osg::Uniform("tex", 0));
289 }
290
291 if (useSRGB) {
292 stateSet->setMode(GL_FRAMEBUFFER_SRGB, osg::StateAttribute::ON);
293 }
294
295 geometry = osg::createTexturedQuadGeometry(osg::Vec3(0.0, 0.0, 0.0),
296 osg::Vec3(1.0, 0.0, 0.0),
297 osg::Vec3(0.0, 1.0, 0.0));
298 geometry->setSupportsDisplayList(false);
299
300 _splashFSQuadColor = new osg::Vec4Array;
301 _splashFSQuadColor->push_back(osg::Vec4(1, 1.0f, 1, 1));
302 geometry->setColorArray(_splashFSQuadColor);
303 geometry->setColorBinding(osg::Geometry::BIND_OVERALL);
304
305 stateSet = geometry->getOrCreateStateSet();
306 stateSet->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON);
307 stateSet->setTextureAttribute(0, _splashFBOTexture);
308
309 geode = new osg::Geode;
310 geode->addDrawable(geometry);
311
312#ifdef ENABLE_OSGXR
313 _splashSwapchain->attachToMirror(stateSet);
314#endif
315 _splashQuadCamera->addChild(geode);
316 addChild(_splashQuadCamera);
317}
318
319// creates a text element from a property node;
320// <text> defines the text
321// <text-prop> overrides the text with the contents of a property
322// <dynamic-text> will link to a property and update the screen when the property changes
323// <condition> Visibility Expression
324// <color> <r><g><b> </color> define colour to be used
325// <font> specifies the font alignment, size and typeface
326// <max-width> [optional] the max normalized width of the element
327// <max-height> [optional] the max normalized height of the element.
328// <max-lines> [optional] the max number of lines this text can be wrapped over
329// wrapping takes place at max-width
330//
331void SplashScreen::CreateTextFromNode(const SGPropertyNode_ptr& content, osg::Geode* geode, bool modelContent)
332{
333 auto text = content->getStringValue("text", "");
334 std::string textFromProperty = content->getStringValue("text-prop", "");
335 if (!textFromProperty.empty())
336 text = fgGetString(textFromProperty);
337
338 SGPropertyNode* dynamicValueNode = nullptr;
339 std::string dynamicProperty = content->getStringValue("dynamic-text");
340 if (!dynamicProperty.empty())
341 dynamicValueNode = fgGetNode(dynamicProperty, true);
342
343 auto conditionNode = content->getChild("condition");
344 SGCondition* condition = nullptr;
345
346 if (conditionNode != nullptr) {
347 condition = sgReadCondition(fgGetNode("/"), conditionNode);
348 }
349 auto x = content->getDoubleValue("x", 0.5);
350 auto y = content->getDoubleValue("y", 0.5);
351
352 if (modelContent) {
353 // the top 0.2 of the screen is for system usage
354 if (y < 0.2) {
355 SG_LOG(SG_VIEW, SG_ALERT, "model content cannot be above 0.2 y");
356 y = 0.2;
357 }
358 }
359
360 auto textItem = addText(geode, osg::Vec2(x, y),
361 content->getDoubleValue("font/size", 0.06),
362 text,
363 osgutils::mapAlignment(content->getStringValue("font/alignment", "left-top")),
364 dynamicValueNode,
365 content->getDoubleValue("max-width", -1.0),
366 osg::Vec4(content->getDoubleValue("color/r", 1), content->getDoubleValue("color/g", 1), content->getDoubleValue("color/b", 1), content->getDoubleValue("color/a", 1)),
367 content->getStringValue("font/face", "Fonts/LiberationFonts/LiberationSans-BoldItalic.ttf"));
368
369 textItem->condition = condition;
370
371 if (textItem->condition != nullptr && !textItem->condition->test())
372 textItem->textNode->setDrawMode(0);
373
374
375 auto maxHeight = content->getDoubleValue("max-height", -1.0);
376 auto maxLineCount = content->getIntValue("max-line-count", -1);
377
378 if (maxLineCount > 0)
379 textItem->maxLineCount = maxLineCount;
380 if (maxHeight > 0)
381 textItem->maxHeightFraction = maxHeight;
382}
383
384osg::ref_ptr<osg::Camera> SplashScreen::createFBOCamera()
385{
386 osg::ref_ptr<osg::Camera> c = new osg::Camera;
387 c->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
388 c->setViewMatrix(osg::Matrix::identity());
389 c->setClearMask( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
390 c->setClearColor( osg::Vec4( 0., 0., 0., 0. ) );
391 c->setAllowEventFocus(false);
392 c->setCullingActive(false);
393 c->setRenderTargetImplementation( osg::Camera::FRAME_BUFFER_OBJECT );
394 c->setRenderOrder(osg::Camera::PRE_RENDER);
395 c->attach(osg::Camera::COLOR_BUFFER, _splashFBOTexture);
396#ifdef ENABLE_OSGXR
397 _splashSwapchain->attachToCamera(c);
398#endif
399
400 osg::StateSet* stateSet = c->getOrCreateStateSet();
401 stateSet->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
402 stateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);
403
404 bool use_vertex_attribute_aliasing = false;
406 if (guiCamera) {
407 auto gc = guiCamera->getGraphicsContext();
408 use_vertex_attribute_aliasing = gc->getState()->getUseVertexAttributeAliasing();
409 }
410 if (use_vertex_attribute_aliasing) {
411 stateSet->setAttribute(_program);
412 stateSet->addUniform(new osg::Uniform("tex", 0));
413 }
414
415 return c;
416}
417
418// Load an image for display
419// - path
420// - x,y,width,height (normalized coordinates)
421// - isBackground flag to indicate that this image is the background image. Only set during one call to this method when loading the splash image
422//
423const SplashScreen::ImageItem *SplashScreen::addImage(const std::string &path, bool isAbsolutePath, double x, double y, double width, double height, SGPropertyNode*
424 conditionNode, bool isBackground)
425{
426 if (path.empty())
427 return nullptr;
428
429 SGPath imagePath;
430
431 if (!isAbsolutePath)
432 imagePath = globals->resolve_maybe_aircraft_path(path);
433 else
434 imagePath = path;
435
436 if (!imagePath.exists() || !imagePath.isFile()) {
437 SG_LOG(SG_VIEW, SG_INFO, "Splash Image " << path << " not be found");
438 return nullptr;
439 }
440
441 ImageItem item;
442 item.name = path;
443 item.x = x;
444 item.y = y;
445 item.height = height;
446 item.width = width;
447 item.isBackground = isBackground;
448
449 if (conditionNode != nullptr)
450 item.condition = sgReadCondition(fgGetNode("/"), conditionNode);
451 else
452 item.condition = nullptr;
453
454 osg::ref_ptr<simgear::SGReaderWriterOptions> staticOptions = simgear::SGReaderWriterOptions::copyOrCreate(osgDB::Registry::instance()->getOptions());
455 staticOptions->setLoadOriginHint(simgear::SGReaderWriterOptions::LoadOriginHint::ORIGIN_SPLASH_SCREEN);
456
457 item.Image = osgDB::readRefImageFile(imagePath.utf8Str(), staticOptions);
458 if (!item.Image) {
459 SG_LOG(SG_VIEW, SG_INFO, "Splash Image " << imagePath << " failed to load");
460 return nullptr;
461 }
462
463 item.imageWidth = item.Image->s();
464 item.imageHeight = item.Image->t();
465 item.aspectRatio = static_cast<double>(item.imageWidth) / item.imageHeight;
466 if (item.height == 0 && item.imageWidth != 0)
467 item.height = item.imageHeight * (item.width / item.imageWidth);
468
469 osg::Texture2D* imageTexture = new osg::Texture2D(item.Image);
470 imageTexture->setResizeNonPowerOfTwoHint(false);
471 imageTexture->setInternalFormat(GL_RGBA);
472 imageTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR);
473 imageTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);
474
475 osg::Geometry* geometry = new osg::Geometry;
476 geometry->setSupportsDisplayList(false);
477
478 item.vertexArray = new osg::Vec3Array;
479 for (int i=0; i < 4; ++i) {
480 item.vertexArray->push_back(osg::Vec3(0.0, 0.0, 0.0));
481 }
482 geometry->setVertexArray(item.vertexArray);
483
484 osg::Vec2Array* imageTextureCoordinates = new osg::Vec2Array;
485 imageTextureCoordinates->push_back(osg::Vec2(0, 0));
486 imageTextureCoordinates->push_back(osg::Vec2(1.0, 0));
487 imageTextureCoordinates->push_back(osg::Vec2(1.0, 1.0));
488 imageTextureCoordinates->push_back(osg::Vec2(0, 1.0));
489 geometry->setTexCoordArray(0, imageTextureCoordinates);
490
491 osg::Vec4Array* imageColorArray = new osg::Vec4Array;
492 imageColorArray->push_back(osg::Vec4(1, 1, 1, 1));
493 geometry->setColorArray(imageColorArray);
494 geometry->setColorBinding(osg::Geometry::BIND_OVERALL);
495 geometry->addPrimitiveSet(new osg::DrawArrays(GL_TRIANGLE_FAN, 0, 4));
496
497 osg::StateSet* stateSet = geometry->getOrCreateStateSet();
498 stateSet->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON);
499 stateSet->setTextureAttribute(0, imageTexture);
500 stateSet->setMode(GL_BLEND, osg::StateAttribute::ON);
501
502 osg::Geode* geode = new osg::Geode;
503 _splashFBOCamera->addChild(geode);
504 geode->addDrawable(geometry);
505
506 item.geode = geode;
507 item.nodeMask = geode->getNodeMask();
508
509 if (item.condition != nullptr && !item.condition->test())
510 item.geode->setNodeMask(0);
511
512 _imageItems.push_back(item);
513 return &_imageItems.back();
514}
515
516SplashScreen::TextItem *SplashScreen::addText(osg::Geode* geode ,
517 const osg::Vec2& pos, double size, const std::string& text,
518 const osgText::Text::AlignmentType alignment,
519 SGPropertyNode* dynamicValue,
520 double maxWidthFraction,
521 const osg::Vec4& textColor,
522 const std::string &fontFace )
523{
524 SGPath path = globals->resolve_maybe_aircraft_path(fontFace);
525
526 TextItem item;
527 osg::ref_ptr<osgText::Text> t = new osgText::Text;
528 item.textNode = t;
529 t->setFont(path.utf8Str());
530 t->setColor(textColor);
531 t->setFontResolution(64, 64);
532 t->setText(text, osgText::String::Encoding::ENCODING_UTF8);
533 t->setBackdropType(osgText::Text::OUTLINE);
534 t->setBackdropColor(osg::Vec4(0.2, 0.2, 0.2, 1));
535 t->setBackdropOffset(0.04);
536
537 item.fractionalCharSize = size;
538 item.fractionalPosition = pos;
539 item.dynamicContent = dynamicValue;
540 item.textNode->setAlignment(alignment);
541 item.maxWidthFraction = maxWidthFraction;
542 item.condition = nullptr; // default to always display.
543 item.drawMode = t->getDrawMode();
544 geode->addDrawable(item.textNode);
545
546 _items.push_back(item);
547 return &_items.back();
548}
549
550void SplashScreen::TextItem::reposition(int width, int height) const
551{
552 const int halfWidth = width >> 1;
553 const int halfHeight = height >> 1;
554 osg::Vec3 pixelPos(fractionalPosition.x() * width - halfWidth,
555 (1.0 - fractionalPosition.y()) * height - halfHeight,
556 -0.1);
557 textNode->setPosition(pixelPos);
558 textNode->setCharacterSize(fractionalCharSize * height);
559
560 if (maxWidthFraction > 0.0) {
561 textNode->setMaximumWidth(maxWidthFraction * width);
562 }
563
564 recomputeSize(height);
565}
566
567void SplashScreen::TextItem::recomputeSize(int height) const
568{
569 if ((maxLineCount == 0) && (maxHeightFraction < 0.0)) {
570 return;
571 }
572
573 double heightFraction = maxHeightFraction;
574 if (heightFraction < 0.0) {
575 heightFraction = 9999.0;
576 }
577
578 double baseSize = fractionalCharSize;
579 textNode->update();
580 while ((textNode->getLineCount() > maxLineCount) ||
581 (baseSize * textNode->getLineCount() > heightFraction)) {
582 baseSize *= 0.8; // 20% shrink each time
583 textNode->setCharacterSize(baseSize * height);
584 textNode->update();
585 }
586}
587
588std::string SplashScreen::selectSplashImage()
589{
590 sg_srandom_time(); // init random seed
591
592 simgear::PropertyList previewNodes = fgGetNode("/sim/previews", true)->getChildren("preview");
593 std::vector<SGPath> paths;
594
595 for (auto n : previewNodes) {
596 if (!n->getBoolValue("splash", false)) {
597 continue;
598 }
599
600 SGPath tpath = globals->resolve_maybe_aircraft_path(n->getStringValue("path"));
601 if (tpath.exists()) {
602 paths.push_back(tpath);
603 }
604 }
605
606 if (paths.empty()) {
607 // look for a legacy aircraft splash
608 simgear::PropertyList nodes = fgGetNode("/sim/startup", true)->getChildren("splash-texture");
609 for (auto n : nodes) {
610 SGPath tpath = globals->resolve_maybe_aircraft_path(n->getStringValue());
611 if (tpath.exists()) {
612 paths.push_back(tpath);
613 _legacySplashScreenMode = true;
614 }
615 }
616 }
617
618 if (!paths.empty()) {
619 // Select a random useable texture
620 const int index = (int)(sg_random() * paths.size());
621 return paths.at(index).utf8Str();
622 }
623
624 // no splash screen specified - use one of the default ones
625 SGPath tpath = globals->get_fg_root() / "Textures";
626 paths = simgear::Dir(tpath).children(simgear::Dir::TYPE_FILE);
627 paths.erase(std::remove_if(paths.begin(), paths.end(), [](const SGPath& p) {
628 const auto f = p.file();
629 if (f.find("Splash") != 0) return true;
630 const auto ext = p.extension();
631 return ext != "png" && ext != "jpg";
632 }), paths.end());
633
634 if (!paths.empty()) {
635 // Select a random useable texture
636 const int index = (int)(sg_random() * paths.size());
637 return paths.at(index).utf8Str();
638 }
639
640 SG_LOG(SG_GUI, SG_ALERT, "Couldn't find any splash screens at all");
641 return {};
642}
643
644void SplashScreen::doUpdate()
645{
647 // don't createNodes until OSG init operations have completed
648 return;
649 }
650
651 double alpha = _splashAlphaNode->getDoubleValue();
652
653 if (alpha <= 0 || !fgGetBool("/sim/startup/splash-screen")) {
654 removeChild(0, getNumChildren());
655 _splashFBOCamera = nullptr;
656 _splashQuadCamera = nullptr;
657#ifdef ENABLE_OSGXR
658 _splashLayer->setVisible(false);
659#endif
660 } else if (getNumChildren() == 0) {
661 createNodes();
662 _splashStartTime.stamp();
663 resize(fgGetInt("/sim/startup/xsize"),
664 fgGetInt("/sim/startup/ysize"));
665#ifdef ENABLE_OSGXR
666 _splashLayer->setVisible(true);
667 _splashSwapchain->setForcedAlpha(alpha);
668#endif
669 } else {
670 (*_splashFSQuadColor)[0] = osg::Vec4(1.0, 1.0, 1.0, _splashAlphaNode->getFloatValue());
671 _splashFSQuadColor->dirty();
672
673 for (const TextItem& item : _items) {
674 if (item.condition != nullptr) {
675
676 if (item.condition->test())
677 item.textNode->setDrawMode(item.drawMode);
678 else
679 item.textNode->setDrawMode(0);
680 }
681 if (item.dynamicContent) {
682 item.textNode->setText(
683 item.dynamicContent->getStringValue(),
684 osgText::String::Encoding::ENCODING_UTF8);
685 }
686 }
687 for (const ImageItem& image : _imageItems) {
688 if (image.condition) {
689 if (!image.condition->test())
690 image.geode->setNodeMask(0);
691 else
692 image.geode->setNodeMask(image.nodeMask);
693 }
694 }
695 updateSplashSpinner();
696 updateTipText();
697#ifdef ENABLE_OSGXR
698 _splashSwapchain->setForcedAlpha(alpha);
699#endif
700 }
701}
702
703float scaleAndOffset(float v, float halfWidth)
704{
705 return halfWidth * ((v * 2.0) - 1.0);
706}
707
708void SplashScreen::updateSplashSpinner()
709{
710 const int elapsedMsec = _splashStartTime.elapsedMSec();
711 float splashSpinnerPos = (elapsedMsec % 2000) / 2000.0f;
712 float endPos = splashSpinnerPos + 0.25f;
713 float wrapStartPos = 0.0f;
714 float wrapEndPos = 0.0f; // no wrapped quad
715 if (endPos > 1.0f) {
716 wrapEndPos = endPos - 1.0f;
717 }
718
719 const float halfWidth = _width * 0.5f;
720 const float halfHeight = _height * 0.5f;
721 const float bottomY = -halfHeight;
722 const float topY = bottomY + 8;
723 const float z = -0.05f;
724
725 splashSpinnerPos = scaleAndOffset(splashSpinnerPos, halfWidth);
726 endPos = scaleAndOffset(endPos, halfWidth);
727 wrapStartPos = scaleAndOffset(wrapStartPos, halfWidth);
728 wrapEndPos = scaleAndOffset(wrapEndPos, halfWidth);
729
730 osg::Vec3 positions[12] = {
731 osg::Vec3(splashSpinnerPos, bottomY, z),
732 osg::Vec3(endPos, bottomY, z),
733 osg::Vec3(endPos, topY, z),
734
735 osg::Vec3(splashSpinnerPos, bottomY, z),
736 osg::Vec3(endPos, topY, z),
737 osg::Vec3(splashSpinnerPos, topY, z),
738
739 osg::Vec3(wrapStartPos, bottomY, z),
740 osg::Vec3(wrapEndPos, bottomY, z),
741 osg::Vec3(wrapEndPos, topY, z),
742
743 osg::Vec3(wrapStartPos, bottomY, z),
744 osg::Vec3(wrapEndPos, topY, z),
745 osg::Vec3(wrapStartPos, topY, z)
746 };
747
748 for (int i = 0; i < 12; ++i) {
749 (*_splashSpinnerVertexArray)[i] = positions[i];
750 }
751
752 _splashSpinnerVertexArray->dirty();
753}
754
755void SplashScreen::updateTipText()
756{
757 // after 5 seconds change the tip; but only do this once.
758 // the tip will be set into a property and this in turn will be
759 // displayed by the content using a dynamic-text element
760 if (!_haveSetStartupTip && (_splashStartTime.elapsedMSec() > 5000)) {
761 _haveSetStartupTip = true;
762 FGLocale* locale = globals->get_locale();
763 const int tipCount = locale->getLocalizedStringCount("tip", "tips");
764 if (tipCount == 0) {
765 return;
766 }
767
768 int tipIndex = globals->get_props()->getIntValue("/sim/session",0) % tipCount;
769
770 std::string tipText = locale->getLocalizedStringWithIndex("tip", "tips", tipIndex);
771 fgSetString("/sim/startup/tip", tipText);
772 }
773}
774
775void SplashScreen::resize( int width, int height )
776{
777 if (getNumChildren() == 0) {
778 return;
779 }
780
781 _width = width;
782 _height = height;
783
784 _splashQuadCamera->setViewport(0, 0, width, height);
785 _splashFBOCamera->resizeAttachments(width, height);
786 _splashFBOCamera->setViewport(0, 0, width, height);
787 _splashFBOCamera->setProjectionMatrixAsOrtho2D(-width * 0.5, width * 0.5,
788 -height * 0.5, height * 0.5);
789#ifdef ENABLE_OSGXR
790 float aspect = (float)width / height;
791 _splashSwapchain->setSize(width, height);
792 _splashLayer->setSize(osg::Vec2f(aspect, 1.0f));
793#endif
794
795 const double screenAspectRatio = static_cast<double>(width) / height;
796
797 // resize all of the images on the splash screen (including the background)
798 for (const auto& _imageItem : _imageItems) {
799
800 if (_imageItem.isBackground) {
801 // background is based around the centre of the screen
802 // and adjusted so that the largest of width,height is used
803 // to fill the screen so that the image fits without distortion
804 double halfWidth = width * 0.5;
805 double halfHeight = height * 0.5;
806
807 // if this is the background image and we are in legacy mode then
808 // resize to keep the image scaled to fit in the centre
809 if (_legacySplashScreenMode) {
810 halfWidth = width * 0.35;
811 halfHeight = height * 0.35;
812
813 if (screenAspectRatio > _imageItem.aspectRatio) {
814 // screen is wider than our image
815 halfWidth = halfHeight;
816 }
817 else {
818 // screen is taller than our image
819 halfHeight = halfWidth;
820 }
821 }
822 else {
823 // adjust vertex positions; image covers entire area
824 if (screenAspectRatio > _imageItem.aspectRatio) {
825 // screen is wider than our image
826 halfHeight = halfWidth / _imageItem.aspectRatio;
827 }
828 else {
829 // screen is taller than our image
830 halfWidth = halfHeight * _imageItem.aspectRatio;
831 }
832 }
833 (*_imageItem.vertexArray)[0] = osg::Vec3(-halfWidth, -halfHeight, 0.0);
834 (*_imageItem.vertexArray)[1] = osg::Vec3(halfWidth, -halfHeight, 0.0);
835 (*_imageItem.vertexArray)[2] = osg::Vec3(halfWidth, halfHeight, 0.0);
836 (*_imageItem.vertexArray)[3] = osg::Vec3(-halfWidth, halfHeight, 0.0);
837 }
838 else {
839
840 float imageWidth = _imageItem.width * width;
841 float imageHeight = _imageItem.imageHeight * (imageWidth / _imageItem.imageWidth);
842
843 float imageX = _imageItem.x * (width - imageWidth);
844 float imageY = (1.0 - _imageItem.y) * (height - imageHeight);
845
846 float originX = imageX - (width * 0.5);
847 float originY = imageY - (height * 0.5);
848
849 (*_imageItem.vertexArray)[0] = osg::Vec3(originX, originY, 0.0);
850 (*_imageItem.vertexArray)[1] = osg::Vec3(originX + imageWidth, originY, 0.0);
851 (*_imageItem.vertexArray)[2] = osg::Vec3(originX + imageWidth, originY + imageHeight, 0.0);
852 (*_imageItem.vertexArray)[3] = osg::Vec3(originX, originY + imageHeight, 0.0);
853 }
854 _imageItem.vertexArray->dirty();
855 }
856
857 // adjust all text positions.
858 for (const TextItem& item : _items) {
859 item.reposition(width, height);
860 }
861}
862
863void fgSplashProgress( const char *identifier, unsigned int percent )
864{
865 fgSetString("/sim/startup/splash-progress-spinner", "");
866
867 std::string text;
868 if (identifier[0] != 0)
869 {
870 text = globals->get_locale()->getLocalizedString(identifier, "sys");
871
872 if( text.empty() )
873 text = "<incomplete language resource>: "s + identifier;
874 }
875
876 if (!strcmp(identifier,"downloading-scenery")) {
877
878 // get localized texts for units
879 std::string kbytesUnitText = globals->get_locale()->getLocalizedString("units-kbytes", "sys", "KB");
880 std::string mbytesUnitText = globals->get_locale()->getLocalizedString("units-mbytes", "sys", "MB");
881 std::string kbytesPerSecUnitText = globals->get_locale()->getLocalizedString("units-kbytes-per-sec", "sys", "KB/s");
882 std::string mbytesPerSecUnitText = globals->get_locale()->getLocalizedString("units-mbytes-per-sec", "sys", "MB/s");
883
884 std::ostringstream oss;
885 unsigned int kbytesPerSec = fgGetInt("/sim/terrasync/transfer-rate-bytes-sec") / 1024;
886 unsigned int kbytesPending = fgGetInt("/sim/terrasync/pending-kbytes");
887 unsigned int kbytesPendingExtract = fgGetInt("/sim/terrasync/extract-pending-kbytes");
888 if (kbytesPending > 0) {
889 if (kbytesPending > 1024) {
890 int mBytesPending = kbytesPending >> 10;
891 oss << " " << mBytesPending << " "s << mbytesUnitText;
892 } else {
893 oss << " " << kbytesPending << " "s << kbytesUnitText;
894 }
895 }
896
897 if (kbytesPerSec > 100) {
898 double mbytesPerSec = kbytesPerSec / 1024.0;
899 oss << " - " << std::fixed << std::setprecision(1) << mbytesPerSec << " "s << mbytesPerSecUnitText;
900 } else if (kbytesPerSec > 0) {
901 oss << " - " << kbytesPerSec << " "s << kbytesPerSecUnitText;
902 } else if (kbytesPendingExtract > 0) {
903 const std::string extractText = globals->get_locale()->getLocalizedString("scenery-extract", "sys");
904 std::ostringstream os2;
905
906 if (kbytesPendingExtract > 1024) {
907 int mBytesPendingExtract = kbytesPendingExtract >> 10;
908 os2 << mBytesPendingExtract << " "s << mbytesUnitText;
909 } else {
910 os2 << kbytesPendingExtract << " "s << kbytesUnitText;
911 }
912 auto finalText = simgear::strutils::replace(extractText, "[VALUE]", os2.str());
913 oss << " - " << finalText;
914 }
915 fgSetString("/sim/startup/splash-progress-spinner", oss.str());
916 }
917
918 if (!strcmp(identifier, "loading-scenery")) {
919
920 // get localized texts for units
921 std::string kbytesUnitText = globals->get_locale()->getLocalizedString("units-kbytes", "sys", "KB");
922 std::string mbytesUnitText = globals->get_locale()->getLocalizedString("units-mbytes", "sys", "MB");
923
924 unsigned int kbytesPendingExtract = fgGetInt("/sim/terrasync/extract-pending-kbytes");
925 if (kbytesPendingExtract > 0) {
926 const std::string extractText = globals->get_locale()->getLocalizedString("scenery-extract", "sys");
927 std::ostringstream oss;
928 if (kbytesPendingExtract > 1024) {
929 int mBytesPendingExtract = kbytesPendingExtract >> 10;
930 oss << mBytesPendingExtract << " "s << mbytesUnitText;
931 } else {
932 oss << kbytesPendingExtract << " "s << kbytesUnitText;
933 }
934
935 auto finalText = simgear::strutils::replace(extractText, "[VALUE]", oss.str());
936 fgSetString("/sim/startup/splash-progress-spinner", finalText);
937 } else {
938 fgSetString("/sim/startup/splash-progress-spinner", "");
939 }
940 }
941
942 // over-write the spinner
943 if (!strncmp(identifier, "navdata-", 8)) {
944 const std::string percentText = globals->get_locale()->getLocalizedString("navdata-load-percent", "sys");
945 auto finalText = simgear::strutils::replace(percentText, "[VALUE]", std::to_string(percent));
946 fgSetString("/sim/startup/splash-progress-spinner", finalText);
947 }
948
949 if( fgGetString("/sim/startup/splash-progress-text") == text )
950 return;
951
952 SG_LOG( SG_VIEW, SG_INFO, "Splash screen progress " << identifier );
953 fgSetString("/sim/startup/splash-progress-text", text);
954}
#define p(x)
#define i(x)
SGPath resolve_maybe_aircraft_path(const std::string &branch) const
Same as above, but test for non 'Aircraft/' branch paths, and always resolve them against fg_root.
Definition globals.cxx:560
virtual FGRenderer * get_renderer() const
Definition globals.cxx:572
SGPropertyNode * get_props()
Definition globals.hxx:320
FGLocale * get_locale()
Definition globals.hxx:328
const SGPath & get_fg_root() const
Definition globals.hxx:189
std::string getLocalizedStringWithIndex(const std::string &id, const std::string &context, int index) const
Obtain a single translation with the given identifier, context and index.
Definition locale.cxx:525
std::size_t getLocalizedStringCount(const std::string &id, const std::string &context) const
Return the number of strings with a given id in the specified context.
Definition locale.cxx:547
std::string getLocalizedString(const std::string &id, const std::string &resource, const std::string &defaultValue={})
Obtain a single string matching the given id, with fallback.
Definition locale.cxx:533
bool runInitOperation()
Run a graphics operation that retrieves some OpenGL parameters.
Definition renderer.cxx:457
virtual void operator()(osg::Node *node, osg::NodeVisitor *nv)
Definition splash.cxx:57
void resize(int width, int height)
Definition splash.cxx:775
friend class SplashScreenUpdateCallback
Definition splash.hxx:94
static CameraGroup * getDefault()
Get the default CameraGroup.
int fgGetInt(const char *name, int defaultValue)
Get an int value for a property.
Definition fg_props.cxx:532
std::string fgGetString(const char *name, const char *defaultValue)
Get a string value for a property.
Definition fg_props.cxx:556
FGGlobals * globals
Definition globals.cxx:142
FlightGear Localization Support.
std::string getAircraftAuthorsText()
getAircraftAuthorsText - get the aircraft authors as a single string value.
Definition util.cxx:187
osg::Camera * getGUICamera(CameraGroup *cgroup)
Get the osg::Camera that draws the GUI, if any, from a camera group.
bool fgGetBool(char const *name, bool def)
Get a bool value for a property.
Definition proptest.cpp:25
bool fgSetBool(char const *name, bool val)
Set a bool value for a property.
Definition proptest.cpp:24
double fgGetDouble(const char *name, double defaultValue)
Get a double value for a property.
Definition proptest.cpp:30
bool fgSetString(char const *name, char const *str)
Set a string value for a property.
Definition proptest.cpp:26
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27
float scaleAndOffset(float v, float halfWidth)
Definition splash.cxx:703
static const char * LICENSE_URL_TEXT
Definition splash.cxx:50
void fgSplashProgress(const char *identifier, unsigned int percent=0)
Set progress information.
Definition splash.cxx:863