2
Copyright (C) 2004 Erik Hjortsberg
4
This program is free software; you can redistribute it and/or modify
5
it under the terms of the GNU General Public License as published by
6
the Free Software Foundation; either version 2 of the License, or
7
(at your option) any later version.
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
GNU General Public License for more details.
14
You should have received a copy of the GNU General Public License
15
along with this program; if not, write to the Free Software
16
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
#include "AvatarCamera.h"
22
#include "AvatarTerrainCursor.h"
24
#include "EmberOgre.h"
25
#include "EmberEntity.h"
26
// #include "WorldEmberEntity.h"
27
#include "MathConverter.h"
30
#include "services/EmberServices.h"
31
#include "services/config/ConfigService.h"
32
#include "services/time/TimeService.h"
34
#include "services/sound/SoundService.h"
37
#include "MousePicker.h"
38
// #include "jesus/JesusPickerObject.h"
40
#include "SceneManagers/EmberPagingSceneManager/include/OgrePagingLandScapeRaySceneQuery.h"
41
#include "framework/Tokeniser.h"
42
#include "framework/ConsoleBackend.h"
44
#include "GUIManager.h"
45
#include "services/input/Input.h"
47
#include "IWorldPickListener.h"
49
#include "framework/osdir.h"
56
using namespace Ember;
59
Recorder::Recorder(): mSequence(0), mAccruedTime(0.0f), mFramesPerSecond(20.0f)
63
void Recorder::startRecording()
65
Ogre::Root::getSingleton().addFrameListener(this);
67
void Recorder::stopRecording()
69
Ogre::Root::getSingleton().removeFrameListener(this);
72
bool Recorder::frameStarted(const Ogre::FrameEvent& event)
74
mAccruedTime += event.timeSinceLastFrame;
75
if (mAccruedTime >= (1.0f / mFramesPerSecond)) {
77
std::stringstream filename;
78
filename << "screenshot_" << mSequence++ << ".tga";
79
const std::string dir = Ember::EmberServices::getSingletonPtr()->getConfigService()->getHomeDirectory() + "/recordings/";
81
//make sure the directory exists
85
ret = stat( dir.c_str(), &tagStat );
90
mkdir(dir.c_str(), S_IRWXU);
93
} catch (const std::exception& ex) {
94
S_LOG_FAILURE("Error when creating directory for screenshots. Message: " << std::string(ex.what()));
100
EmberOgre::getSingleton().getRenderWindow()->writeContentsToFile(dir + filename.str());
101
} catch (const Ogre::Exception& ex) {
102
S_LOG_FAILURE("Could not write screenshot to disc. Message: "<< ex.getFullDescription());
112
AvatarCamera::AvatarCamera(Ogre::SceneNode* avatarNode, Ogre::SceneManager& sceneManager, Ogre::RenderWindow& window, Input& input, Ogre::Camera& camera) :
113
SetCameraDistance("setcameradistance", this, "Set the distance of the camera."),
114
ToggleRendermode("toggle_rendermode", this, "Toggle between wireframe and solid render modes."),
115
ToggleFullscreen("toggle_fullscreen", this, "Switch between windowed and full screen mode."),
116
Screenshot("screenshot", this, "Take a screenshot and write to disk."),
117
Record("+record", this, "Record to disk."),
118
mInvertCamera(false),
121
mSceneManager(sceneManager),
122
mDegreeOfPitchPerSecond(50),
123
mDegreeOfYawPerSecond(50),
128
mClosestPickingDistance(10000),
129
mLastPosition(Ogre::Vector3::ZERO),
130
mAdjustTerrainRaySceneQuery(0),
131
mCameraRaySceneQuery(0),
132
mIsAdjustedToTerrain(true),
133
mAvatarTerrainCursor(new AvatarTerrainCursor(*this))
134
// mLastOrientationOfTheCamera(avatar->getOrientation())
136
createNodesForCamera();
138
setAvatarNode(avatarNode);
140
/// Register this as a frame listener
141
Ogre::Root::getSingleton().addFrameListener(this);
143
input.EventMouseMoved.connect(sigc::mem_fun(*this, &AvatarCamera::Input_MouseMoved));
145
Ember::EmberServices::getSingletonPtr()->getConfigService()->EventChangedConfigItem.connect(sigc::mem_fun(*this, &AvatarCamera::ConfigService_EventChangedConfigItem));
147
updateValuesFromConfig();
151
AvatarCamera::~AvatarCamera()
153
Ogre::Root::getSingleton().removeFrameListener(this);
154
EmberOgre::getSingleton().getSceneManager()->destroyQuery(mAdjustTerrainRaySceneQuery);
155
EmberOgre::getSingleton().getSceneManager()->destroyQuery(mCameraRaySceneQuery);
158
void AvatarCamera::createRayQueries()
160
// attempt to create a query to get back terrain coords
161
mAdjustTerrainRaySceneQuery = EmberOgre::getSingletonPtr()->getSceneManager()->createRayQuery(mAdjustTerrainRay, Ogre::SceneManager::WORLD_GEOMETRY_TYPE_MASK);
162
///only test for terrain
163
mAdjustTerrainRaySceneQuery->setWorldFragmentType(Ogre::SceneQuery::WFT_SINGLE_INTERSECTION);
164
mAdjustTerrainRaySceneQuery->setSortByDistance(true);
165
mAdjustTerrainRaySceneQuery->setQueryTypeMask(Ogre::SceneManager::WORLD_GEOMETRY_TYPE_MASK);
168
unsigned long queryMask = Ogre::SceneManager::WORLD_GEOMETRY_TYPE_MASK;
169
queryMask |= MousePicker::CM_AVATAR;
170
queryMask |= MousePicker::CM_ENTITY;
171
queryMask |= MousePicker::CM_NATURE;
172
queryMask |= MousePicker::CM_UNDEFINED;
173
// queryMask |= Ogre::RSQ_FirstTerrain;
175
mCameraRaySceneQuery = mSceneManager.createRayQuery( Ogre::Ray(), queryMask);
176
mCameraRaySceneQuery->setWorldFragmentType(Ogre::SceneQuery::WFT_SINGLE_INTERSECTION);
177
mCameraRaySceneQuery->setSortByDistance(true);
182
void AvatarCamera::createNodesForCamera()
184
mAvatarCameraRootNode = mSceneManager.createSceneNode("AvatarCameraRootNode");
185
//we need to adjust for the height of the avatar mesh
186
mAvatarCameraRootNode->setPosition(Ogre::Vector3(0,2,0));
187
//rotate to sync with WF world
188
mAvatarCameraRootNode->rotate(Ogre::Vector3::UNIT_Y,(Ogre::Degree)-90);
190
mAvatarCameraPitchNode = mAvatarCameraRootNode->createChildSceneNode("AvatarCameraPitchNode");
191
mAvatarCameraPitchNode->setPosition(Ogre::Vector3(0,0,0));
192
mAvatarCameraNode = mAvatarCameraPitchNode->createChildSceneNode("AvatarCameraNode");
193
setCameraDistance(10);
195
// mCamera = mSceneManager.createCamera("AvatarCamera");
196
mAvatarCameraNode->attachObject(&mCamera);
197
// Look to the Avatar's head
198
//mAvatar3pCamera->setAutoTracking(true, mAvatar1pCameraNode);
199
mCamera.setNearClipDistance(0.5);
201
///set the far clip distance high to make sure that the sky is completely shown
202
if (Ogre::Root::getSingleton().getRenderSystem()->getCapabilities()->hasCapability(Ogre::RSC_INFINITE_FAR_PLANE))
204
/* //NOTE: this won't currently work with the sky
205
mCamera.setFarClipDistance(0);*/
207
mCamera.setFarClipDistance(10000);
209
mCamera.setFarClipDistance(10000);
212
//create the nodes for the camera
213
setMode(MODE_THIRD_PERSON);
217
void AvatarCamera::setMode(Mode mode)
220
/* if (mMode == MODE_THIRD_PERSON) {
221
mCamera.setAutoTracking(true, mAvatarCameraRootNode);
223
mCamera.setAutoTracking(false);
229
const Ogre::Quaternion& AvatarCamera::getOrientation(bool onlyHorizontal) const {
230
if (!onlyHorizontal) {
231
return getCamera().getDerivedOrientation();
233
static Ogre::Quaternion quat;
234
quat = getCamera().getDerivedOrientation();
241
const Ogre::Vector3& AvatarCamera::getPosition() const
243
return mCamera.getDerivedPosition();
246
void AvatarCamera::attach(Ogre::SceneNode* toNode) {
248
assert(mAvatarCameraRootNode);
249
if (mAvatarCameraRootNode->getParent()) {
250
mAvatarCameraRootNode->getParent()->removeChild(mAvatarCameraRootNode->getName());
252
toNode->addChild(mAvatarCameraRootNode);
254
setCameraDistance(10);
255
mAvatarCameraNode->setOrientation(Ogre::Quaternion::IDENTITY);
256
mAvatarCameraNode->_update(true, true);
257
std::stringstream ss;
258
ss << "Attached camera to node: " << toNode->getName() <<". New position: " << mCamera.getDerivedPosition() << " New orientation: " << mCamera.getDerivedOrientation();
259
S_LOG_VERBOSE(ss.str());
263
void AvatarCamera::enableCompositor(const std::string& compositorName, bool enable)
265
if (std::find(mLoadedCompositors.begin(), mLoadedCompositors.end(), compositorName) == mLoadedCompositors.end()) {
266
Ogre::CompositorManager::getSingleton().addCompositor(mWindow.getViewport(0), compositorName);
268
Ogre::CompositorManager::getSingleton().setCompositorEnabled(mWindow.getViewport(0), compositorName, enable);
271
void AvatarCamera::createViewPort()
274
// Ogre::CompositorManager::getSingleton().addCompositor(mWindow.getViewport(0), "Bloom");
275
// Ogre::CompositorManager::getSingleton().setCompositorEnabled(mWindow.getViewport(0), "Bloom", true);
278
// assert(!mViewPort);
279
// // Create 1st person viewport, entire window
280
// mViewPort = mWindow.addViewport(mCamera);
281
// mViewPort->setBackgroundColour(Ogre::ColourValue(0,0,0));
282
// mCamera.setAspectRatio(
283
// Ogre::Real(mViewPort->getActualWidth()) / Ogre::Real(mViewPort->getActualHeight()));
289
void AvatarCamera::toggleRenderMode()
291
S_LOG_INFO("Switching render mode.");
292
if (mCamera.getPolygonMode() == Ogre::PM_SOLID) {
293
mCamera.setPolygonMode(Ogre::PM_WIREFRAME);
295
mCamera.setPolygonMode(Ogre::PM_SOLID);
299
void AvatarCamera::setAvatarNode(Ogre::SceneNode* sceneNode)
301
mAvatarNode = sceneNode;
305
void AvatarCamera::setCameraDistance(Ogre::Real distance)
307
mWantedCameraDistance = distance;
308
_setCameraDistance(distance);
311
void AvatarCamera::_setCameraDistance(Ogre::Real distance)
313
mCurrentCameraDistance = distance;
314
Ogre::Vector3 pos(0,0,distance);
315
mAvatarCameraNode->setPosition(pos);
316
markCameraNodeAsDirty();
317
EventChangedCameraDistance.emit(distance);
320
void AvatarCamera::pitch(Ogre::Degree degrees)
323
degrees -= degrees * 2;
326
Ogre::SceneNode* node(mMode == MODE_THIRD_PERSON ? mAvatarCameraPitchNode : mAvatarCameraNode);
328
///prevent the camera from being turned upside down
329
const Ogre::Quaternion& orientation(node->getOrientation());
330
Ogre::Degree pitch(orientation.getPitch());
331
if ((pitch.valueDegrees() + degrees.valueDegrees()) > 0) {
332
degrees = std::min<float>(degrees.valueDegrees(), 90 - pitch.valueDegrees());
334
degrees = std::max<float>(degrees.valueDegrees(), -90 - pitch.valueDegrees());
337
if (mMode == MODE_THIRD_PERSON) {
338
degreePitch += degrees;
339
node->pitch(degrees);
341
node->pitch(degrees);
343
///We need to manually update the node here to make sure that the derived orientation and position of the camera is updated.
344
node->_update(true, false);
345
markCameraNodeAsDirty();
347
void AvatarCamera::yaw(Ogre::Degree degrees)
349
if (mMode == MODE_THIRD_PERSON) {
350
degreeYaw += degrees;
351
mAvatarCameraRootNode->yaw(degrees);
353
///We need to manually update the node here to make sure that the derived orientation and position of the camera is updated.
354
mAvatarCameraRootNode->_update(true, false);
356
mAvatarCameraNode->yaw(degrees);
357
///We need to manually update the node here to make sure that the derived orientation and position of the camera is updated.
358
mAvatarCameraNode->_update(true, false);
360
markCameraNodeAsDirty();
363
void AvatarCamera::markCameraNodeAsDirty()
365
if (mCamera.getParentNode()) {
366
///We need to mark the parent node of the camera as dirty. The update of the derived orientation and position of the node should normally occur when the scene tree is traversed, but in some instances we need to access the derived position or orientataion of the camera before the traversal occurs, and if we don't mark the node as dirty it won't be updated
367
mCamera.getParentNode()->needUpdate(true);
371
void AvatarCamera::Input_MouseMoved(const MouseMotion& motion, Input::InputMode mode)
372
/*(int xPosition, int yPosition, Ogre::Real xRelativeMovement, Ogre::Real yRelativeMovement, Ogre::Real timeSinceLastMovement)*/
374
if (mode == Input::IM_MOVEMENT) {
375
Ogre::Degree diffX(mDegreeOfYawPerSecond * motion.xRelativeMovement);
376
Ogre::Degree diffY(mDegreeOfPitchPerSecond * motion.yRelativeMovement);
378
if (diffX.valueDegrees()) {
380
// this->yaw(diffX * e->timeSinceLastFrame);
382
if (diffY.valueDegrees()) {
384
// this->pitch(diffY * e->timeSinceLastFrame);
387
if (diffY.valueDegrees() || diffX.valueDegrees()) {
388
MovedCamera.emit(mCamera);
398
void AvatarCamera::pickInWorld(Ogre::Real mouseX, Ogre::Real mouseY, const MousePickerArgs& mousePickerArgs)
400
S_LOG_INFO("Trying to pick an entity at mouse coords: " << Ogre::StringConverter::toString(mouseX) << ":" << Ogre::StringConverter::toString(mouseY) << ".");
402
// get the terrain vector for mouse coords when a pick event happens
403
// mAvatarTerrainCursor->getTerrainCursorPosition();
405
/// Start a new ray query
406
Ogre::Ray cameraRay = getCamera().getCameraToViewportRay( mouseX, mouseY );
408
mCameraRaySceneQuery->setRay(cameraRay);
409
mCameraRaySceneQuery->execute();
412
///now check the entity picking
413
Ogre::RaySceneQueryResult& queryResult = mCameraRaySceneQuery->getLastResults();
414
bool continuePicking = true;
416
for (WorldPickListenersStore::iterator I = mPickListeners.begin(); I != mPickListeners.end(); ++I) {
417
(*I)->initializePickingContext();
421
Ogre::RaySceneQueryResult::iterator rayIterator = queryResult.begin( );
422
Ogre::RaySceneQueryResult::iterator rayIterator_end = queryResult.end( );
423
if (rayIterator != rayIterator_end) {
424
for ( ; rayIterator != rayIterator_end && continuePicking; rayIterator++ ) {
425
for (WorldPickListenersStore::iterator I = mPickListeners.begin(); I != mPickListeners.end(); ++I) {
426
(*I)->processPickResult(continuePicking, *rayIterator, cameraRay, mousePickerArgs);
427
if (!continuePicking) {
434
for (WorldPickListenersStore::iterator I = mPickListeners.begin(); I != mPickListeners.end(); ++I) {
435
(*I)->endPickingContext(mousePickerArgs);
439
bool AvatarCamera::worldToScreen(const Ogre::Vector3& worldPos, Ogre::Vector2& screenPos)
442
Ogre::Vector3 hcsPosition = mCamera.getProjectionMatrix() * (mCamera.getViewMatrix() * worldPos);
444
if ((hcsPosition.x < -1.0f) ||
445
(hcsPosition.x > 1.0f) ||
446
(hcsPosition.y < -1.0f) ||
447
(hcsPosition.y > 1.0f))
451
screenPos.x = (hcsPosition.x + 1) * 0.5;
452
screenPos.y = (-hcsPosition.y + 1) * 0.5;
457
// void AvatarCamera::setClosestPickingDistance(Ogre::Real distance)
459
// mClosestPickingDistance = distance;
462
// Ogre::Real AvatarCamera::getClosestPickingDistance()
464
// return mClosestPickingDistance;
467
bool AvatarCamera::adjustForTerrain()
469
/// We will shoot a ray from the camera base node to the camera. If it hits anything on the way we know that there's something between the camera and the avatar and we'll position the camera closer to the avatar. Thus we'll avoid having the camera dip below the terrain
470
///For now we'll only check against the terrain
471
const Ogre::Vector3 direction(-mCamera.getDerivedDirection());
472
///If the direction if pointing straight upwards we'll end up in an infinite loop in the ray query
473
if (direction.z != 0) {
475
mAdjustTerrainRay.setDirection(direction);
476
mAdjustTerrainRay.setOrigin(mAvatarCameraRootNode->_getDerivedPosition());
478
mAdjustTerrainRaySceneQuery->setRay(mAdjustTerrainRay);
480
mAdjustTerrainRaySceneQuery->execute();
482
Ogre::RaySceneQueryResult queryResult = mAdjustTerrainRaySceneQuery->getLastResults();
483
Ogre::RaySceneQueryResult::iterator rayIterator = queryResult.begin( );
484
for ( ; rayIterator != queryResult.end(); ++rayIterator ) {
485
Ogre::RaySceneQueryResultEntry& entry = *rayIterator;
487
if (entry.worldFragment) {
488
Ogre::Vector3 position = entry.worldFragment->singleIntersection;
489
Ogre::Real distance = mAvatarCameraRootNode->_getDerivedPosition().distance(position);
490
if (distance < mWantedCameraDistance) {
491
_setCameraDistance(distance - 0.1);
494
///we hit some terrain beyond the max distance of the camera, so set it to the "default" distance
495
if (mWantedCameraDistance != mCurrentCameraDistance) {
496
_setCameraDistance(mWantedCameraDistance);
505
/* Ogre::RaySceneQuery *raySceneQueryHeight = EmberOgre::getSingletonPtr()->getSceneManager()->createRayQuery( Ogre::Ray(mCamera.getDerivedPosition(), Ogre::Vector3::NEGATIVE_UNIT_Y), Ogre::SceneManager::WORLD_GEOMETRY_TYPE_MASK);
508
raySceneQueryHeight->execute();
510
//first check the terrain picking
511
Ogre::RaySceneQueryResult queryResult = raySceneQueryHeight->getLastResults();
513
if (queryResult.begin( ) != queryResult.end()) {
514
Ogre::Vector3 position = queryResult.begin()->worldFragment->singleIntersection;
515
Ogre::Real terrainHeight = position.y;
517
//terrainHeight += 0.4;
518
Ogre::Real cameraHeight = mCamera.getDerivedPosition().y;
519
Ogre::Real cameraNodeHeight = mAvatarCameraNode->getWorldPosition().y;
520
if (terrainHeight > cameraHeight) {
521
mCamera.move(mCamera.getDerivedOrientation().Inverse() * Ogre::Vector3(0,terrainHeight - cameraHeight,0));
522
// mCamera.lookAt(mAvatarCameraRootNode->getPosition());
524
} else if (cameraHeight != cameraNodeHeight) {
525
Ogre::Real newHeight = std::max<Ogre::Real>(terrainHeight, cameraNodeHeight);
526
mCamera.move(Ogre::Vector3(0,newHeight - cameraHeight,0));
527
mCamera.lookAt(mAvatarCameraRootNode->getWorldPosition());
535
void AvatarCamera::runCommand(const std::string &command, const std::string &args)
537
if(Screenshot == command) {
538
//just take a screen shot
540
} else if(SetCameraDistance == command)
542
Ember::Tokeniser tokeniser;
543
tokeniser.initTokens(args);
544
std::string distance = tokeniser.nextToken();
545
if (distance != "") {
546
float fDistance = Ogre::StringConverter::parseReal(distance);
547
setCameraDistance(fDistance);
549
} else if (ToggleFullscreen == command){
550
SDL_WM_ToggleFullScreen(SDL_GetVideoSurface());
552
} else if (ToggleRendermode == command) {
554
} else if (Record == command) {
555
mRecorder.startRecording();
556
} else if (Record.getInverseCommand() == command) {
557
mRecorder.stopRecording();
561
void AvatarCamera::updateValuesFromConfig()
563
if (Ember::EmberServices::getSingletonPtr()->getConfigService()->itemExists("input", "invertcamera")) {
564
mInvertCamera = static_cast<bool>(Ember::EmberServices::getSingletonPtr()->getConfigService()->getValue("input", "invertcamera"));
566
if (Ember::EmberServices::getSingletonPtr()->getConfigService()->itemExists("input", "cameradegreespersecond")) {
567
mDegreeOfPitchPerSecond = mDegreeOfYawPerSecond = (double)Ember::EmberServices::getSingletonPtr()->getConfigService()->getValue("input", "cameradegreespersecond");
569
if (Ember::EmberServices::getSingletonPtr()->getConfigService()->itemExists("input", "adjusttoterrain")) {
570
mIsAdjustedToTerrain = static_cast<bool>(Ember::EmberServices::getSingletonPtr()->getConfigService()->getValue("input", "adjusttoterrain"));
574
void AvatarCamera::ConfigService_EventChangedConfigItem(const std::string& section, const std::string& key)
576
if (section == "input") {
577
if (key == "invertcamera" || key == "cameradegreespersecond" || key == "adjusttoterrain") {
578
updateValuesFromConfig();
583
bool AvatarCamera::frameStarted(const Ogre::FrameEvent& event)
585
if (mIsAdjustedToTerrain && mAvatarNode) {
586
if (mCamera.getDerivedPosition() != mLastPosition) {
588
mLastPosition = mCamera.getDerivedPosition();
592
/// Update avatar entity position in sound service
593
Ember::EmberServices::getSingleton().getSoundService()->updateListenerPosition(Ogre2Atlas(mCamera.getDerivedPosition()), Ogre2Atlas_Vector3(mCamera.getDirection()), Ogre2Atlas_Vector3(mCamera.getUp()));
598
void AvatarCamera::pushWorldPickListener(IWorldPickListener* worldPickListener)
600
mPickListeners.push_front(worldPickListener);
603
void AvatarCamera::removeWorldPickListener(IWorldPickListener* worldPickListener)
605
if (worldPickListener) {
606
WorldPickListenersStore::iterator I = std::find(mPickListeners.begin(), mPickListeners.end(), worldPickListener);
607
if (I != mPickListeners.end()) {
608
mPickListeners.erase(I);
614
const std::string AvatarCamera::_takeScreenshot()
616
// retrieve current time
620
timeinfo = localtime(&rawtime);
622
// construct filename string
623
// padding with 0 for single-digit values
624
std::stringstream filename;
625
filename << "screenshot_" << ((*timeinfo).tm_year + 1900); // 1900 is year "0"
626
int month = ((*timeinfo).tm_mon + 1); // January is month "0"
632
int day = (*timeinfo).tm_mday;
637
filename << day << "_";
638
int hour = (*timeinfo).tm_hour;
644
int min = (*timeinfo).tm_min;
650
int sec = (*timeinfo).tm_sec;
655
filename << sec << ".jpg";
657
const std::string dir = Ember::EmberServices::getSingletonPtr()->getConfigService()->getHomeDirectory() + "/screenshots/";
659
//make sure the directory exists
661
oslink::directory osdir(dir);
663
if (!osdir.isExisting()) {
667
mkdir(dir.c_str(), S_IRWXU);
670
} catch (const std::exception& ex) {
671
S_LOG_FAILURE("Error when creating directory for screenshots. Message: " << std::string(ex.what()));
672
throw Ember::Exception("Error when saving screenshot. Message: " + std::string(ex.what()));
677
mWindow.writeContentsToFile(dir + filename.str());
678
} catch (const Ogre::Exception& ex) {
679
S_LOG_FAILURE("Could not write screenshot to disc. Message: "<< ex.getFullDescription());
680
throw Ember::Exception("Error when saving screenshot. Message: " + ex.getDescription());
682
return dir + filename.str();
685
void AvatarCamera::takeScreenshot()
688
const std::string& result = _takeScreenshot();
690
Ember::ConsoleBackend::getSingletonPtr()->pushMessage("Wrote image: " + result);
691
} catch (const std::exception& ex) {
692
Ember::ConsoleBackend::getSingletonPtr()->pushMessage(std::string("Error when saving screenshot: ") + ex.what());
694
Ember::ConsoleBackend::getSingletonPtr()->pushMessage("Unknown error when saving screenshot.");