diff options
Diffstat (limited to 'indra/newview/llfloater360capture.cpp')
-rw-r--r-- | indra/newview/llfloater360capture.cpp | 930 |
1 files changed, 930 insertions, 0 deletions
diff --git a/indra/newview/llfloater360capture.cpp b/indra/newview/llfloater360capture.cpp new file mode 100644 index 0000000000..745f154535 --- /dev/null +++ b/indra/newview/llfloater360capture.cpp @@ -0,0 +1,930 @@ +/** + * @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(); + + // It's not possible (as I had hoped) to always render to an off screen + // buffer regardless of deferred rendering status so the next best + // option is to render to a buffer that is the size of the users app + // window. Note, this was changed - before it chose the smallest + // power of 2 less than the window size - but since that meant a + // 1023 height app window would result in a 512 pixel capture, Maxim + // tried this and it does indeed appear to work. Mayb need to revisit + // after the project viewer pass if people on low end graphics systems + // after having issues. + if (mSourceImageSize > window_width || mSourceImageSize > window_height) + { + mSourceImageSize = llmin(window_width, window_height, mSourceImageSize); + 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 ought 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) +{ + llofstream 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) +{ + llofstream 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); + + llofstream 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); + + bool render_attached_lights = LLPipeline::sRenderAttachedLights; + // 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); + LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_PARTICLES); + LLPipeline::sRenderAttachedLights = FALSE; + } + + // 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; + + // set new parameters specific to the 360 requirements + 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", + }; + + // number of times to render the scene (display(..) inside + // the simple snapshot function in llViewerWindow. + // Note: rendering even just 1 more time (for a total of 2) + // has a dramatic effect on the scene contents and *much* + // less of it is missing. More investigation required + // but for the moment, this helps with missing content + // because of interest list issues. + int num_render_passes = gSavedSettings.getU32("360CaptureNumRenderPasses"); + + // 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, num_render_passes); + + // 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); + LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_PARTICLES); + LLPipeline::sRenderAttachedLights = render_attached_lights; + } + + // 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); + + // page is loaded and ready so we can turn on the buttons again + mCaptureBtn->setEnabled(true); + mSaveLocalBtn->setEnabled(true); + + // allow the UI to update by suspending and waiting for the + // main render loop to update the UI + suspendForAFrame(); +} + +// 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(); +} |