summaryrefslogtreecommitdiff
path: root/indra/newview/llfloater360capture.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'indra/newview/llfloater360capture.cpp')
-rw-r--r--indra/newview/llfloater360capture.cpp904
1 files changed, 904 insertions, 0 deletions
diff --git a/indra/newview/llfloater360capture.cpp b/indra/newview/llfloater360capture.cpp
new file mode 100644
index 0000000000..aa226981aa
--- /dev/null
+++ b/indra/newview/llfloater360capture.cpp
@@ -0,0 +1,904 @@
+/**
+ * @file llfloater360capture.cpp
+ * @author Callum Prentice (callum@lindenlab.com)
+ * @brief Floater code for the 360 Capture feature
+ *
+ * $LicenseInfo:firstyear=2011&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2011, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#include "llviewerprecompiledheaders.h"
+
+#include "llfloater360capture.h"
+
+#include "llagent.h"
+#include "llagentui.h"
+#include "llbase64.h"
+#include "llcallbacklist.h"
+#include "llenvironment.h"
+#include "llimagejpeg.h"
+#include "llmediactrl.h"
+#include "llradiogroup.h"
+#include "llslurl.h"
+#include "lltextbox.h"
+#include "lltrans.h"
+#include "lluictrlfactory.h"
+#include "llversioninfo.h"
+#include "llviewercamera.h"
+#include "llviewercontrol.h"
+#include "llviewerpartsim.h"
+#include "llviewerregion.h"
+#include "llviewerwindow.h"
+#include "pipeline.h"
+
+#include <iterator>
+
+LLFloater360Capture::LLFloater360Capture(const LLSD& key)
+ : LLFloater(key)
+{
+ // The handle to embedded browser that we use to
+ // render the WebGL preview as we as host the
+ // Cube Map to Equirectangular image code
+ mWebBrowser = nullptr;
+
+ // Ask the simulator to send us everything (and not just
+ // what it thinks the connected Viewer can see) until
+ // such time as we ask it not to (the dtor). If we crash or
+ // otherwise, exit before this is turned off, the Simulator
+ // will take care of cleaning up for us.
+ if (gSavedSettings.getBOOL("360CaptureUseInterestListCap"))
+ {
+ // send everything to us for as long as this floater is open
+ const bool send_everything = true;
+ changeInterestListMode(send_everything);
+ }
+}
+
+LLFloater360Capture::~LLFloater360Capture()
+{
+ if (mWebBrowser)
+ {
+ mWebBrowser->navigateStop();
+ mWebBrowser->clearCache();
+ mWebBrowser->unloadMediaSource();
+ }
+
+ // Tell the Simulator not to send us everything anymore
+ // and revert to the regular "keyhole" frustum of interest
+ // list updates.
+ if (gSavedSettings.getBOOL("360CaptureUseInterestListCap"))
+ {
+ const bool send_everything = false;
+ changeInterestListMode(send_everything);
+ }
+}
+
+BOOL LLFloater360Capture::postBuild()
+{
+ mCaptureBtn = getChild<LLUICtrl>("capture_button");
+ mCaptureBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onCapture360ImagesBtn, this));
+
+ mSaveLocalBtn = getChild<LLUICtrl>("save_local_button");
+ mSaveLocalBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onSaveLocalBtn, this));
+ mSaveLocalBtn->setEnabled(false);
+
+ mWebBrowser = getChild<LLMediaCtrl>("360capture_contents");
+ mWebBrowser->addObserver(this);
+
+ // There is a group of radio buttons that define the quality
+ // by each having a 'value' that is returns equal to the pixel
+ // size (width == height)
+ mQualityRadioGroup = getChild<LLRadioGroup>("360_quality_selection");
+ mQualityRadioGroup->setCommitCallback(boost::bind(&LLFloater360Capture::onChooseQualityRadioGroup, this));
+
+ // UX/UI called for preview mode (always the first index/option)
+ // by default each time vs restoring the last value
+ mQualityRadioGroup->setSelectedIndex(0);
+
+ // Construct a URL pointing to the first page to load. Although
+ // we do not use this page for anything (after some significant
+ // design changes), we retain the code to load the start page
+ // in case that changes again one day. It also makes sure the
+ // embedded browser is active and ready to go for when the real
+ // page with the 360 preview is navigated to.
+ std::string url = STRINGIZE(
+ "file:///" <<
+ getHTMLBaseFolder() <<
+ mDefaultHTML
+ );
+ mWebBrowser->navigateTo(url);
+
+ // initial pass at determining what size (width == height since
+ // the cube map images are square) we should capture at.
+ setSourceImageSize();
+
+ // the size of the output equirectangular image. The height of an EQR image
+ // is always 1/2 of the width so we should not store it but rather,
+ // calculate it from the width directly
+ mOutputImageWidth = gSavedSettings.getU32("360CaptureOutputImageWidth");
+ mOutputImageHeight = mOutputImageWidth / 2;
+
+ // enable resizing and enable for width and for height
+ enableResizeCtrls(true, true, true);
+
+ // initial heading that consumers of the equirectangular image
+ // (such as Facebook or Flickr) use to position initial view -
+ // we set during capture - stored as degrees (0..359)
+ mInitialHeadingDeg = 0.0;
+
+ // save directory in which to store the images (must obliviously be
+ // writable by the viewer). Also create it for users who haven't
+ // used the 360 feature before.
+ mImageSaveDir = gDirUtilp->getLindenUserDir() + gDirUtilp->getDirDelimiter() + "eqrimg";
+ LLFile::mkdir(mImageSaveDir);
+
+ // We do an initial capture when the floater is opened, albeit at a 'preview'
+ // quality level (really low resolution, but really fast)
+ onCapture360ImagesBtn();
+
+ return true;
+}
+
+// called when the user choose a quality level using
+// the buttons in the radio group
+void LLFloater360Capture::onChooseQualityRadioGroup()
+{
+ // set the size of the captured cube map images based
+ // on the quality level chosen
+ setSourceImageSize();
+}
+
+// Using a new capability, tell the simulator that we want it to send everything
+// it knows about and not just what is in front of the camera, in its view
+// frustum. We need this feature so that the contents of the region that appears
+// in the 6 snapshots which we cannot see and is normally not "considered", is
+// also rendered. Typically, this is turned on when the 360 capture floater is
+// opened and turned off when it is closed.
+// Note: for this version, we do not have a way to determine when "everything"
+// has arrived and has been rendered so for now, the proposal is that users
+// will need to experiment with the low resolution version and wait for some
+// (hopefully) small period of time while the full contents resolves.
+// Pass in a flag to ask the simulator/interest list to "send everything" or
+// not (the default mode)
+void LLFloater360Capture::changeInterestListMode(bool send_everything)
+{
+ LLSD body;
+
+ if (send_everything)
+ {
+ body["mode"] = LLSD::String("360");
+ }
+ else
+ {
+ body["mode"] = LLSD::String("default");
+ }
+
+ if (gAgent.requestPostCapability("InterestList", body, [](const LLSD & response)
+ {
+ LL_INFOS("360Capture") <<
+ "InterestList capability responded: \n" <<
+ ll_pretty_print_sd(response) <<
+ LL_ENDL;
+ }))
+ {
+ LL_INFOS("360Capture") <<
+ "Successfully posted an InterestList capability request with payload: \n" <<
+ ll_pretty_print_sd(body) <<
+ LL_ENDL;
+ }
+ else
+ {
+ LL_INFOS("360Capture") <<
+ "Unable to post an InterestList capability request with payload: \n" <<
+ ll_pretty_print_sd(body) <<
+ LL_ENDL;
+ }
+}
+
+// There is is a setting (360CaptureSourceImageSize) that holds the size
+// (width == height since it's a square) of each of the 6 source snapshots.
+// However there are some known (and I dare say, some more unknown conditions
+// where the specified size is not possible and this function tries to figure it
+// out and change that setting to the optimal value for the current conditions.
+void LLFloater360Capture::setSourceImageSize()
+{
+ mSourceImageSize = mQualityRadioGroup->getSelectedValue().asInteger();
+
+ // If deferred rendering is off, we need to shrink the window we capture
+ // until it's smaller than the Viewer window dimensions.
+ if (!LLPipeline::sRenderDeferred)
+ {
+ LLRect window_rect = gViewerWindow->getWindowRectRaw();
+ S32 window_width = window_rect.getWidth();
+ S32 window_height = window_rect.getHeight();
+
+ while (mSourceImageSize > window_width || mSourceImageSize > window_height)
+ {
+ mSourceImageSize /= 2;
+ LL_INFOS("360Capture") << "Deferred rendering is forcing a smaller capture size: " << mSourceImageSize << LL_ENDL;
+ }
+
+ // there has to be an easier way than this to get the value
+ // from the radio group item at index 0. Why doesn't
+ // LLRadioGroup::getSelectedValue(int index) exist?
+ int index = mQualityRadioGroup->getSelectedIndex();
+ mQualityRadioGroup->setSelectedIndex(0);
+ int min_size = mQualityRadioGroup->getSelectedValue().asInteger();
+ mQualityRadioGroup->setSelectedIndex(index);
+
+ // If the maximum size we can support falls below a threshold then
+ // we should display a message in the log so we can try to debug
+ // why this is happening
+ if (mSourceImageSize < min_size)
+ {
+ LL_INFOS("360Capture") << "Small snapshot size due to deferred rendering and small app window" << LL_ENDL;
+ }
+ }
+}
+
+// This function shouldn't exist! We use the tooltip text from
+// the corresponding XUI file (floater_360capture.xml) as the
+// overlay text for the final web page to inform the user
+// about the quality level in play. There ouught to be a
+// UI function like LLView* getSelectedItemView() or similar
+// but as far as I can tell, there isn't so we have to resort
+// to finding it ourselves with this icky code..
+const std::string LLFloater360Capture::getSelectedQualityTooltip()
+{
+ // safey (or bravery?)
+ if (mQualityRadioGroup != nullptr)
+ {
+ // for all the child widgets for the radio group
+ // (just the radio buttons themselves I think)
+ for (child_list_const_reverse_iter_t iter = mQualityRadioGroup->getChildList()->rbegin();
+ iter != mQualityRadioGroup->getChildList()->rend();
+ ++iter)
+ {
+ // if we match the selected index (which we can get easily)
+ // with our position in the list of children
+ if (mQualityRadioGroup->getSelectedIndex() ==
+ std::distance(mQualityRadioGroup->getChildList()->rend(), iter) - 1)
+ {
+ // return the plain old tooltip text
+ return (*iter)->getToolTip();
+ }
+ }
+ }
+
+ // if it's not found or not available, return an empty string
+ return std::string();
+}
+
+// Some of the 'magic' happens via a web page in an HTML directory
+// and this code provides a single point of reference for its' location
+const std::string LLFloater360Capture::getHTMLBaseFolder()
+{
+ std::string folder_name = gDirUtilp->getSkinDir();
+ folder_name += gDirUtilp->getDirDelimiter();
+ folder_name += "html";
+ folder_name += gDirUtilp->getDirDelimiter();
+ folder_name += "common";
+ folder_name += gDirUtilp->getDirDelimiter();
+ folder_name += "equirectangular";
+ folder_name += gDirUtilp->getDirDelimiter();
+
+ return folder_name;
+}
+
+// triggered when the 'capture' button in the UI is pressed
+void LLFloater360Capture::onCapture360ImagesBtn()
+{
+ // launch the main capture code in a coroutine so we can
+ // yield/suspend at some points to give the main UI
+ // thread a look-in occasionally.
+ LLCoros::instance().launch("capture360cap", [this]()
+ {
+ capture360Images();
+ });
+}
+
+// Gets the full path name for a given JavaScript file in the HTML folder. We
+// use this ultimately as a parameter to the main web app so it knows where to find
+// the JavaScript array containing the 6 cube map images, stored as data URLs
+const std::string LLFloater360Capture::makeFullPathToJS(const std::string filename)
+{
+ std::string full_js_path = mImageSaveDir;
+ full_js_path += gDirUtilp->getDirDelimiter();
+ full_js_path += filename;
+
+ return full_js_path;
+}
+
+// Write the header/prequel portion of the JavaScript array of data urls
+// that we use to store the cube map images in (so the web code can load
+// them without tweaking browser security - we'd have to do this if they
+// we stored as plain old images) This deliberately overwrites the old
+// one, if it exists
+void LLFloater360Capture::writeDataURLHeader(const std::string filename)
+{
+ std::ofstream file_handle(filename.c_str());
+ if (file_handle.is_open())
+ {
+ file_handle << "// cube map images for Second Life Viewer panorama 360 images" << std::endl;
+ file_handle.close();
+ }
+}
+
+// Write the footer/sequel portion of the JavaScript image code. When this is
+// called, the current file on disk will contain the header and the 6 data
+// URLs, each with a well specified name. This final piece of JavaScript code
+// creates an array from those data URLs that the main application can
+// reference and read.
+void LLFloater360Capture::writeDataURLFooter(const std::string filename)
+{
+ std::ofstream file_handle(filename.c_str(), std::ios_base::app);
+ if (file_handle.is_open())
+ {
+ file_handle << "var cubemap_img_js = [" << std::endl;
+ file_handle << " img_posx, img_negx," << std::endl;
+ file_handle << " img_posy, img_negy," << std::endl;
+ file_handle << " img_posz, img_negz," << std::endl;
+ file_handle << "];" << std::endl;
+
+ file_handle.close();
+ }
+}
+
+// Given a filename, a chunk of data (representing an image file) and the size
+// of the buffer, we create a BASE64 encoded string and use it to build a JavaScript
+// data URL that represents the image in a web browser environment
+bool LLFloater360Capture::writeDataURL(const std::string filename, const std::string prefix, U8* data, unsigned int data_len)
+{
+ LL_INFOS("360Capture") << "Writing data URL for " << prefix << " to " << filename << LL_ENDL;
+
+ const std::string data_url = LLBase64::encode(data, data_len);
+
+ std::ofstream file_handle(filename.c_str(), std::ios_base::app);
+ if (file_handle.is_open())
+ {
+ file_handle << "var img_";
+ file_handle << prefix;
+ file_handle << " = '";
+ file_handle << "data:image/jpeg; base64,";
+ file_handle << data_url;
+ file_handle << "'";
+ file_handle << std::endl;
+ file_handle.close();
+
+ return true;
+ }
+
+ return false;
+}
+
+// Encode the image from each of the 6 snapshots and save it out to
+// the JavaScript array of data URLs
+void LLFloater360Capture::encodeAndSave(LLPointer<LLImageRaw> raw_image, const std::string filename, const std::string prefix)
+{
+ // the default quality for the JPEG encoding is set quite high
+ // but this still seems to be a reasonable compromise for
+ // quality/size and is still much smaller than equivalent PNGs
+ int jpeg_encode_quality = gSavedSettings.getU32("360CaptureJPEGEncodeQuality");
+ LLPointer<LLImageJPEG> jpeg_image = new LLImageJPEG(jpeg_encode_quality);
+
+ // Actually encode the JPEG image. This is where a lot of time
+ // is spent now that the snapshot capture process has been
+ // optimized. The encode_time parameter doesn't appear to be
+ // used anymore.
+ const int encode_time = 0;
+ bool resultjpeg = jpeg_image->encode(raw_image, encode_time);
+
+ if (resultjpeg)
+ {
+ // save individual cube map images as real JPEG files
+ // for debugging or curiosity) based on debug settings
+ if (gSavedSettings.getBOOL("360CaptureDebugSaveImage"))
+ {
+ const std::string jpeg_filename = STRINGIZE(
+ gDirUtilp->getLindenUserDir() <<
+ gDirUtilp->getDirDelimiter() <<
+ "eqrimg" <<
+ gDirUtilp->getDirDelimiter() <<
+ prefix <<
+ "." <<
+ jpeg_image->getExtension()
+ );
+
+ LL_INFOS("360Capture") << "Saving debug JPEG image as " << jpeg_filename << LL_ENDL;
+ jpeg_image->save(jpeg_filename);
+ }
+
+ // actually write the JPEG image to disk as a data URL
+ writeDataURL(filename, prefix, jpeg_image->getData(), jpeg_image->getDataSize());
+ }
+}
+
+// Defer back to the main loop for a single rendered frame to give
+// the renderer a chance to update the UI if it is needed
+void LLFloater360Capture::suspendForAFrame()
+{
+ const U32 frame_count_delta = 1;
+ U32 curr_frame_count = LLFrameTimer::getFrameCount();
+ while (LLFrameTimer::getFrameCount() <= curr_frame_count + frame_count_delta)
+ {
+ llcoro::suspend();
+ }
+}
+
+// A debug version of the snapshot code that simply fills the
+// buffer with a pattern that can be used to investigate
+// issues with encoding and saving off each RAW image.
+// Probably not needed anymore but saving here just in case.
+void LLFloater360Capture::mockSnapShot(LLImageRaw* raw)
+{
+ unsigned int width = raw->getWidth();
+ unsigned int height = raw->getHeight();
+ unsigned int depth = raw->getComponents();
+ unsigned char* pixels = raw->getData();
+
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ unsigned long offset = y * width * depth + x * depth;
+ unsigned char red = x * 256 / width;
+ unsigned char green = y * 256 / height;
+ unsigned char blue = ((x + y) / 2) * 256 / (width + height) / 2;
+ pixels[offset + 0] = red;
+ pixels[offset + 1] = green;
+ pixels[offset + 2] = blue;
+ }
+ }
+}
+
+// The main code that actually captures all 6 images and then saves them out to
+// disk before navigating the embedded web browser to the page with the WebGL
+// application that consumes them and creates an EQR image. This code runs as a
+// coroutine so it can be suspended at certain points.
+void LLFloater360Capture::capture360Images()
+{
+ // recheck the size of the cube map source images in case it changed
+ // since it was set when we opened the floater
+ setSourceImageSize();
+
+ // disable buttons while we are capturing
+ mCaptureBtn->setEnabled(false);
+ mSaveLocalBtn->setEnabled(false);
+
+ // determine whether or not to include avatar in the scene as we capture the 360 panorama
+ if (gSavedSettings.getBOOL("360CaptureHideAvatars"))
+ {
+ // Turn off the avatar if UI tells us to hide it.
+ // Note: the original call to gAvatar.hide(FALSE) did *not* hide
+ // attachments and so for most residents, there would be some debris
+ // left behind in the snapshot.
+ // Note: this toggles so if it set to on, this will turn it off and
+ // the subsequent call to the same thing after capture is finished
+ // will turn it back on again. Similarly, for the case where it
+ // was set to off - I think this is what we need
+ LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_AVATAR);
+ }
+
+ // these are the 6 directions we will point the camera - essentially,
+ // North, South, East, West, Up, Down
+ LLVector3 look_dirs[6] = { LLVector3(1, 0, 0), LLVector3(0, 1, 0), LLVector3(0, 0, 1), LLVector3(-1, 0, 0), LLVector3(0, -1, 0), LLVector3(0, 0, -1) };
+ LLVector3 look_upvecs[6] = { LLVector3(0, 0, 1), LLVector3(0, 0, 1), LLVector3(0, -1, 0), LLVector3(0, 0, 1), LLVector3(0, 0, 1), LLVector3(0, 1, 0) };
+
+ // save current view/camera settings so we can restore them afterwards
+ S32 old_occlusion = LLPipeline::sUseOcclusion;
+ LLPipeline::sUseOcclusion = 0;
+ LLViewerCamera* camera = LLViewerCamera::getInstance();
+ F32 old_fov = camera->getView();
+ F32 old_aspect = camera->getAspect();
+ F32 old_yaw = camera->getYaw();
+
+ // stop the motion of as much of the world moving as much as we can
+ freezeWorld(true);
+
+ // Save the direction (in degrees) the camera is looking when we
+ // take the shot since that is what we write to image metadata
+ // 'GPano:InitialViewHeadingDegrees' field.
+ // We need to convert from the angle getYaw() gives us into something
+ // the XMP data field wants (N=0, E=90, S=180, W= 270 etc.)
+ mInitialHeadingDeg = (360 + 90 - (int)(camera->getYaw() * RAD_TO_DEG)) % 360;
+ LL_INFOS("360Capture") << "Recording a heading of " << (int)(mInitialHeadingDeg) << LL_ENDL;
+
+ // camera constants for the square, cube map capture image
+ camera->setAspect(1.0); // must set aspect ratio first to avoid undesirable clamping of vertical FoV
+ camera->setView(F_PI_BY_TWO);
+ camera->yaw(0.0);
+
+ // record how many times we changed camera to try to understand the "all shots are the same issue"
+ unsigned int camera_changed_times = 0;
+
+ // the name of the JavaScript file written out that contains the 6 cube map images
+ // stored as a JavaScript array of data URLs. If you change this filename, you must
+ // also change the corresponding entry in the HTML file that uses it -
+ // (newview/skins/default/html/common/equirectangular/display_eqr.html)
+ const std::string cumemap_js_filename("cubemap_img.js");
+
+ // construct the full path to this file - typically stored in the users'
+ // Second Life settings / username / eqrimg folder.
+ const std::string cubemap_js_full_path = makeFullPathToJS(cumemap_js_filename);
+
+ // Write the JavaScript file header (the top of the file before the
+ // declarations of the actual data URLs array). In practice, all this writes
+ // is a comment - it's main purpose is to reset the file from the last time
+ // it was written
+ writeDataURLHeader(cubemap_js_full_path);
+
+ // the names of the prefixes we assign as the name to each data URL and are then
+ // consumed by the WebGL application. Nominally, they stand for positive and
+ // negative in the X/Y/Z directions.
+ static const std::string prefixes[6] =
+ {
+ "posx", "posz", "posy",
+ "negx", "negz", "negy",
+ };
+
+ // time the encode process for later optimization
+ auto encode_time_total = 0.0;
+
+ // for each of the 6 directions we shoot...
+ for (int i = 0; i < 6; i++)
+ {
+ // these buffers are where the raw, captured pixels are stored and
+ // the first time we use them, we have to make a new one
+ if (mRawImages[i] == nullptr)
+ {
+ mRawImages[i] = new LLImageRaw(mSourceImageSize, mSourceImageSize, 3);
+ }
+ else
+ // subsequent capture with floater open so we resize the buffer from
+ // the previous run
+ {
+ // LLImageRaw deletes the old one via operator= but just to be
+ // sure, we delete its' large data member first...
+ mRawImages[i]->deleteData();
+ mRawImages[i] = new LLImageRaw(mSourceImageSize, mSourceImageSize, 3);
+ }
+
+ // set up camera to look in each direction
+ camera->lookDir(look_dirs[i], look_upvecs[i]);
+
+ // record if camera changed to try to understand the "all shots are the same issue"
+ if (camera->isChanged())
+ {
+ ++camera_changed_times;
+ }
+
+ // call the (very) simplified snapshot code that simply deals
+ // with a single image, no sub-images etc. but is very fast
+ gViewerWindow->simpleSnapshot(mRawImages[i], mSourceImageSize, mSourceImageSize);
+
+ // encode each image and write to disk while saving how long it took to do so
+ auto t_start = std::chrono::high_resolution_clock::now();
+ encodeAndSave(mRawImages[i], cubemap_js_full_path, prefixes[i]);
+ auto t_end = std::chrono::high_resolution_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::duration<double>>(t_end - t_start);
+ encode_time_total += duration.count();
+
+ // ping the main loop in case the snapshot process takes a really long
+ // time and we get disconnected
+ LLAppViewer::instance()->pingMainloopTimeout("LLFloater360Capture::capture360Images");
+ }
+
+ // display time to encode all 6 images. It tends to be a fairly linear
+ // time for each so we don't need to worry about displaying the time
+ // for each - this gives us plenty to use for optimizing
+ LL_INFOS("360Capture") <<
+ "Time to encode and save 6 images was " <<
+ encode_time_total <<
+ " seconds" <<
+ LL_ENDL;
+
+ // Write the JavaScript file footer (the bottom of the file after the
+ // declarations of the actual data URLs array). The footer comprises of
+ // a JavaScript array declaration that references the 6 data URLs generated
+ // previously and is what is referred to in the display HTML file
+ // (newview/skins/default/html/common/equirectangular/display_eqr.html)
+ writeDataURLFooter(cubemap_js_full_path);
+
+ // unfreeze the world now we have our shots
+ freezeWorld(false);
+
+ // restore original view/camera/avatar settings settings
+ camera->setAspect(old_aspect);
+ camera->setView(old_fov);
+ camera->yaw(old_yaw);
+ LLPipeline::sUseOcclusion = old_occlusion;
+
+ // if we toggled off the avatar because the Hide check box was ticked,
+ // we should toggle it back to where it was before we started the capture
+ if (gSavedSettings.getBOOL("360CaptureHideAvatars"))
+ {
+ LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_AVATAR);
+ }
+
+ // record that we missed some shots in the log for later debugging
+ // note: we use 5 and not 6 because the first shot isn't regarded
+ // as a change - only the subsequent 5 are
+ if (camera_changed_times < 5)
+ {
+ LL_INFOS("360Capture") << "Warning: we only captured " << camera_changed_times << " images." << LL_ENDL;
+ }
+
+ // now we have the 6 shots saved in a well specified location,
+ // we can load the web content that uses them
+ std::string url = "file:///" + getHTMLBaseFolder() + mEqrGenHTML;
+ mWebBrowser->navigateTo(url);
+
+ // allow the UI to update by suspending and waiting for the
+ // main render loop to update the UI
+ suspendForAFrame();
+
+ // page is loaded and ready so we can turn on the buttons again
+ mCaptureBtn->setEnabled(true);
+ mSaveLocalBtn->setEnabled(true);
+}
+
+// once the request is made to navigate to the web page containing the code
+// to process the 6 images into an EQR one, we have to wait for it to finish
+// loaded - we get a "navigate complete" event when that happens that we can act on
+void LLFloater360Capture::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event)
+{
+ switch (event)
+ {
+ // not used right now but retaining because this event might
+ // be useful for a feature I am hoping to add
+ case MEDIA_EVENT_LOCATION_CHANGED:
+ break;
+
+ // navigation in the browser completed
+ case MEDIA_EVENT_NAVIGATE_COMPLETE:
+ {
+ // Confirm that the navigation event does indeed apply to the
+ // page we are looking for. At the moment, this is the only
+ // one we care about so the test is superfluous but that might change.
+ std::string navigate_url = self->getNavigateURI();
+ if (navigate_url.find(mEqrGenHTML) != std::string::npos)
+ {
+ // this string is being passed across to the web so replace all the windows backslash
+ // characters with forward slashes or (I think) the backslashes are treated as escapes
+ std::replace(mImageSaveDir.begin(), mImageSaveDir.end(), '\\', '/');
+
+ // we store the camera FOV (field of view) in a saved setting since this feels
+ // like something it would be interesting to change and experiment with
+ int camera_fov = gSavedSettings.getU32("360CaptureCameraFOV");
+
+ // compose the overlay for the final web page that tells the user
+ // what level of quality the capture was taken with
+ std::string overlay_label = "'" + getSelectedQualityTooltip() + "'";
+
+ // so now our page is loaded and images are in place - call
+ // the JavaScript init script with some parameters to initialize
+ // the WebGL based preview
+ const std::string cmd = STRINGIZE(
+ "init("
+ << mOutputImageWidth
+ << ", "
+ << mOutputImageHeight
+ << ", "
+ << "'"
+ << mImageSaveDir
+ << "'"
+ << ", "
+ << camera_fov
+ << ", "
+ << LLViewerCamera::getInstance()->getYaw()
+ << ", "
+ << overlay_label
+ << ")"
+ );
+
+ // execute the command on the page
+ mWebBrowser->getMediaPlugin()->executeJavaScript(cmd);
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+// called when the user wants to save the cube maps off to the final EQR image
+void LLFloater360Capture::onSaveLocalBtn()
+{
+ // region name and URL
+ std::string region_name; // no sensible default
+ std::string region_url("http://secondlife.com");
+ LLViewerRegion* region = gAgent.getRegion();
+ if (region)
+ {
+ // region names can (and do) contain characters that would make passing
+ // them into a JavaScript function problematic - single quotes for example
+ // so we must escape/encode both
+ region_name = region->getName();
+
+ // escaping/encoding is a minefield - let's just remove any offending characters from the region name
+ region_name.erase(std::remove(region_name.begin(), region_name.end(), '\''), region_name.end());
+ region_name.erase(std::remove(region_name.begin(), region_name.end(), '\"'), region_name.end());
+
+ // fortunately there is already an escaping function built into the SLURL generation code
+ LLSLURL slurl;
+ bool is_escaped = true;
+ LLAgentUI::buildSLURL(slurl, is_escaped);
+ region_url = slurl.getSLURLString();
+ }
+
+ // build suggested filename (the one that appears as the default
+ // in the Save dialog box)
+ const std::string suggested_filename = generate_proposed_filename();
+
+ // This string (the name of the product plus a truncated version number (no build))
+ // is used in the XMP block as the name of the generating and stitching software.
+ // We save the version number here and not in the more generic 'software' item
+ // because that might help us determine something about the image in the future.
+ const std::string client_version = STRINGIZE(
+ LLVersionInfo::instance().getChannel() <<
+ " " <<
+ LLVersionInfo::instance().getShortVersion()
+ );
+
+ // save the time the image was created. I don't know if this should be
+ // UTC/ZULU or the users' local time. It probably doesn't matter.
+ std::time_t result = std::time(nullptr);
+ std::string ctime_str = std::ctime(&result);
+ std::string time_str = ctime_str.substr(0, ctime_str.length() - 1);
+
+ // build the JavaScript data structure that is used to pass all the
+ // variables into the JavaScript function on the web page loaded into
+ // the embedded browser component of the floater.
+ const std::string xmp_details = STRINGIZE(
+ "{ " <<
+ "pano_version: '" << "2.2.1" << "', " <<
+ "software: '" << LLVersionInfo::instance().getChannel() << "', " <<
+ "capture_software: '" << client_version << "', " <<
+ "stitching_software: '" << client_version << "', " <<
+ "width: " << mOutputImageWidth << ", " <<
+ "height: " << mOutputImageHeight << ", " <<
+ "heading: " << mInitialHeadingDeg << ", " <<
+ "actual_source_image_size: " << mQualityRadioGroup->getSelectedValue().asInteger() << ", " <<
+ "scaled_source_image_size: " << mSourceImageSize << ", " <<
+ "first_photo_date: '" << time_str << "', " <<
+ "last_photo_date: '" << time_str << "', " <<
+ "region_name: '" << region_name << "', " <<
+ "region_url: '" << region_url << "', " <<
+ " }"
+ );
+
+ // build the JavaScript command to send to the web browser
+ const std::string cmd = "saveAsEqrImage(\"" + suggested_filename + "\", " + xmp_details + ")";
+
+ // send it to the browser instance, triggering the equirectangular capture
+ // process and complimentary offer to save the image
+ mWebBrowser->getMediaPlugin()->executeJavaScript(cmd);
+}
+
+// We capture all 6 images sequentially and if parts of the world are moving
+// E.G. clouds, water, objects - then we may get seams or discontinuities
+// when the images are combined to form the EQR image. This code tries to
+// stop everything so we can shoot for seamless shots. There is probably more
+// we can do here - e.g. waves in the water probably won't line up.
+void LLFloater360Capture::freezeWorld(bool enable)
+{
+ static bool clouds_scroll_paused = false;
+ if (enable)
+ {
+ // record the cloud scroll current value so we can restore it
+ clouds_scroll_paused = LLEnvironment::instance().isCloudScrollPaused();
+
+ // stop the clouds moving
+ LLEnvironment::instance().pauseCloudScroll();
+
+ // freeze all avatars
+ LLCharacter* avatarp;
+ for (std::vector<LLCharacter*>::iterator iter = LLCharacter::sInstances.begin();
+ iter != LLCharacter::sInstances.end(); ++iter)
+ {
+ avatarp = *iter;
+ mAvatarPauseHandles.push_back(avatarp->requestPause());
+ }
+
+ // freeze everything else
+ gSavedSettings.setBOOL("FreezeTime", true);
+
+ // disable particle system
+ LLViewerPartSim::getInstance()->enable(false);
+ }
+ else // turning off freeze world mode, either temporarily or not.
+ {
+ // restart the clouds moving if they were not paused before
+ // we starting using the 360 capture floater
+ if (clouds_scroll_paused == false)
+ {
+ LLEnvironment::instance().resumeCloudScroll();
+ }
+
+ // thaw all avatars
+ mAvatarPauseHandles.clear();
+
+ // thaw everything else
+ gSavedSettings.setBOOL("FreezeTime", false);
+
+ //enable particle system
+ LLViewerPartSim::getInstance()->enable(true);
+ }
+}
+
+// Build the default filename that appears in the Save dialog box. We try
+// to encode some metadata about too (region name, EQR dimensions, capture
+// time) but the user if free to replace this with anything else before
+// the images is saved.
+const std::string LLFloater360Capture::generate_proposed_filename()
+{
+ std::ostringstream filename("");
+
+ // base name
+ filename << "sl360_";
+
+ LLViewerRegion* region = gAgent.getRegion();
+ if (region)
+ {
+ // this looks complex but it's straightforward - removes all non-alpha chars from a string
+ // which in this case is the SL region name - we use it as a proposed filename but the user is free to change
+ std::string region_name = region->getName();
+ std::replace_if(region_name.begin(), region_name.end(), std::not1(std::ptr_fun(isalnum)), '_');
+ if (region_name.length() > 0)
+ {
+ filename << region_name;
+ filename << "_";
+ }
+ }
+
+ // add in resolution to make it easier to tell what you captured later
+ filename << mOutputImageWidth;
+ filename << "x";
+ filename << mOutputImageHeight;
+ filename << "_";
+
+ // Add in the size of the source image (width == height since it was square)
+ // Might be useful later for quality comparisons
+ filename << mSourceImageSize;
+ filename << "_";
+
+ // add in the current HH-MM-SS (with leading 0's) so users can easily save many shots in same folder
+ std::time_t cur_epoch = std::time(nullptr);
+ std::tm* tm_time = std::localtime(&cur_epoch);
+ filename << std::setfill('0') << std::setw(4) << (tm_time->tm_year + 1900);
+ filename << std::setfill('0') << std::setw(2) << (tm_time->tm_mon + 1);
+ filename << std::setfill('0') << std::setw(2) << tm_time->tm_mday;
+ filename << "_";
+ filename << std::setfill('0') << std::setw(2) << tm_time->tm_hour;
+ filename << std::setfill('0') << std::setw(2) << tm_time->tm_min;
+ filename << std::setfill('0') << std::setw(2) << tm_time->tm_sec;
+
+ // the unusual way we save the output image (originates in the
+ // embedded browser and not the C++ code) means that the system
+ // appends ".jpeg" to the file automatically on macOS at least,
+ // so we only need to do it ourselves for windows.
+#if LL_WINDOWS
+ filename << ".jpg";
+#endif
+
+ return filename.str();
+}