/** * @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 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("capture_button"); mCaptureBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onCapture360ImagesBtn, this)); mSaveLocalBtn = getChild("save_local_button"); mSaveLocalBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onSaveLocalBtn, this)); mSaveLocalBtn->setEnabled(false); mWebBrowser = getChild("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("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); return true; } void LLFloater360Capture::onOpen(const LLSD& key) { // 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(); } // 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 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 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>(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::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(); }