phoenix-firestorm/indra/newview/llfloater360capture.cpp

954 lines
40 KiB
C++

/**
* @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 (!LLApp::isExiting() && 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);
mWebBrowser->setAllowFileDownload(true);
// 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
// <FS:Ansariel> UX/UI has no clue what the users actually want!
//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()
{
// <FS:Ansariel> It's in the default skin folder...
//std::string folder_name = gDirUtilp->getSkinDir();
std::string folder_name = gDirUtilp->getDefaultSkinDir();
// </FS:Ansariel>
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()
{
// <FS:Beq> FIRE-31942 Avoid CoRo that appears to never usefully yield
// Allow option to re-enable on the off chance a low power machine can benefit
if(gSavedSettings.getBOOL("FSUseCoRoFor360Capture"))
{
// </FS:Beq>
// 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();
});
// <FS:Beq> FIRE-31942 Avoid CoRo that appears to never usefully yield
}
else
{
capture360Images();
}
// </FS:Beq>
}
// 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
if(gSavedSettings.getBOOL("FSUseCoRoFor360Capture")) // <FS:Beq/> FIRE-31942 - make apparently pointless CoRo optional (just in case)
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(),
[](char c){ return ! std::isalnum(c); },
'_');
if (! region_name.empty())
{
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();
}