Files
scummvm/video/qtvr_decoder.cpp
T
Malhar 2af98018a9 VIDEO: QTVR: Memory Optimization for Upscaled Panorama
Previous approach used two Surface Objects to store the 1D and 2D
upscaled panorama used while projecting the panorama in higher
quailty respectively causing higher memory usage

Instead use only one Surface object to store the upscaled panorama
which stores both 1D and 2D upscaled panorama as and if needed.

Minor fixes and formatting
2025-04-26 19:36:15 +08:00

2010 lines
61 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
//
// Partially based on ffmpeg code.
//
// Copyright (c) 2001 Fabrice Bellard.
// First version by Francois Revol revol@free.fr
// Seek function by Gael Chardon gael.dev@4now.net
//
#include "video/qt_decoder.h"
#include "video/qt_data.h"
#include "audio/audiostream.h"
#include "common/archive.h"
#include "common/config-manager.h"
#include "common/debug.h"
#include "common/events.h"
#include "common/file.h"
#include "common/keyboard.h"
#include "common/memstream.h"
#include "common/system.h"
#include "common/textconsole.h"
#include "common/timer.h"
#include "common/util.h"
#include "common/compression/unzip.h"
#include "graphics/cursorman.h"
#include "image/icocur.h"
#include "image/png.h"
// Video codecs
#include "image/codecs/codec.h"
namespace Video {
static const char * const MACGUI_DATA_BUNDLE = "macgui.dat";
static void repeatCallback(void *data);
////////////////////////////////////////////
// QuickTimeDecoder methods related to QTVR
////////////////////////////////////////////
static float readAppleFloatField(Common::SeekableReadStream *stream) {
int16 a = stream->readSint16BE();
uint16 b = stream->readUint16BE();
float value = (float)a + (float)b / 65536.0f;
return value;
}
QuickTimeDecoder::PanoSampleDesc::PanoSampleDesc(Common::QuickTimeParser::Track *parentTrack, uint32 codecTag) : Common::QuickTimeParser::SampleDesc(parentTrack, codecTag) {
}
QuickTimeDecoder::PanoSampleDesc::~PanoSampleDesc() {
}
//
// Panorama Track Sample Description
//
// Source: https://developer.apple.com/library/archive/technotes/tn/tn1035.html
Common::QuickTimeParser::SampleDesc *QuickTimeDecoder::readPanoSampleDesc(Common::QuickTimeParser::Track *track, uint32 format, uint32 descSize) {
PanoSampleDesc *entry = new PanoSampleDesc(track, format);
entry->_majorVersion = _fd->readSint16BE(); // must be zero, also observed to be 1
entry->_minorVersion = _fd->readSint16BE();
entry->_sceneTrackID = _fd->readSint32BE();
entry->_loResSceneTrackID = _fd->readSint32BE();
_fd->read(entry->_reserved3, 4 * 6);
entry->_hotSpotTrackID = _fd->readSint32BE();
_fd->read(entry->_reserved4, 4 * 9);
entry->_hPanStart = readAppleFloatField(_fd);
entry->_hPanEnd = readAppleFloatField(_fd);
entry->_vPanTop = readAppleFloatField(_fd);
entry->_vPanBottom = readAppleFloatField(_fd);
entry->_minimumZoom = readAppleFloatField(_fd);
entry->_maximumZoom = readAppleFloatField(_fd);
// info for the highest res version of scene track
entry->_sceneSizeX = _fd->readUint32BE();
entry->_sceneSizeY = _fd->readUint32BE();
entry->_numFrames = _fd->readUint32BE();
entry->_reserved5 = _fd->readSint16BE();
entry->_sceneNumFramesX = _fd->readSint16BE();
entry->_sceneNumFramesY = _fd->readSint16BE();
entry->_sceneColorDepth = _fd->readSint16BE();
// info for the highest rest version of hotSpot track
entry->_hotSpotSizeX = _fd->readSint32BE(); // pixel width of the hot spot panorama
entry->_hotSpotSizeY = _fd->readSint32BE(); // pixel height of the hot spot panorama
entry->_reserved6 = _fd->readSint16BE();
entry->_hotSpotNumFramesX = _fd->readSint16BE(); // diced frames wide
entry->_hotSpotNumFramesY = _fd->readSint16BE(); // dices frame high
entry->_hotSpotColorDepth = _fd->readSint16BE(); // must be 8
if (entry->_minimumZoom == 0.0)
entry->_minimumZoom = 5.0;
if (entry->_maximumZoom == 0.0)
entry->_maximumZoom = 65.0;
return entry;
}
void QuickTimeDecoder::closeQTVR() {
delete _dataBundle;
_dataBundle = nullptr;
cleanupCursors();
if (_repeatTimerActive) {
_repeatTimerActive = false;
g_system->getTimerManager()->removeTimerProc(&repeatCallback);
}
}
void QuickTimeDecoder::renderHotspots(bool mode) {
_renderHotspots = mode;
((PanoTrackHandler *)getTrack(_panoTrack->targetTrack))->setDirty();
}
void QuickTimeDecoder::setQuality(float quality) {
_quality = CLIP<float>(quality, 0.0f, 4.0f);
// 4.0 Highest quality rendering. The rendered image is fully anti-aliased.
//
// 2.0 The rendered image is partially anti-aliased.
//
// 1.0 The rendered image is not anti-aliased.
//
// 0.0 Images are rendered at quality 1.0 when the user is interactively
// panning or zooming, and are automatically updated at quality
// 4.0 during idle time when the user stops panning or zooming.
// All other updates are rendered at quality 4.0.
PanoTrackHandler *track = ((PanoTrackHandler *)getTrack(_panoTrack->targetTrack));
track->setDirty();
}
void QuickTimeDecoder::setWarpMode(int warpMode) {
_warpMode = CLIP(warpMode, 0, 2);
// 2 Two-dimensional warping. This produces perspectively correct
// images from a panoramic source picture.
//
// 1 One-dimensional warping.
//
// 0 No warping. This reproduces the source panorama directly,
// without warping it at all.
PanoTrackHandler *track = ((PanoTrackHandler *)getTrack(_panoTrack->targetTrack));
track->setDirty();
}
void QuickTimeDecoder::setTransitionMode(Common::String mode) {
if (mode.equalsIgnoreCase("swing")) {
_transitionMode = kTransitionModeSwing;
} else {
_transitionMode = kTransitionModeNormal;
}
// normal The new view is imaged and displayed. No transition effect is
// used. The user sees a "cut" from the current view to the new
// view.
//
// swing If the new view is in the current node, the view point moves
// smoothly from the current view to the new view, taking the
// shortest path if the panorama wraps around. The speed of the
// swing is controlled by the transitionSpeed property. If the new
// view is in a different node, the new view is imaged and
// displayed with a "normal" transition.
((PanoTrackHandler *)getTrack(_panoTrack->targetTrack))->setDirty();
}
void QuickTimeDecoder::setTransitionSpeed(float speed) {
_transitionSpeed = speed;
// The TransitionSpeed is a floating point quantity that provides the slowest swing transition
// at 1.0 and faster transitions at higher values. On mid-range computers, a rate of 4.0
// performs well off CD-ROM.
//
// If the TransitionSpeed property value is set to a negative number, swing updates will act
// the same as normal updates
((PanoTrackHandler *)getTrack(_panoTrack->targetTrack))->setDirty();
}
Common::String QuickTimeDecoder::getUpdateMode() const {
switch (_updateMode) {
case kUpdateModeUpdateBoth:
return "updateBoth";
case kUpdateModeOffscreenOnly:
return "offscreenOnly";
case kUpdateModeFromOffscreen:
return "fromOffscreen";
case kUpdateModeDirectToScreen:
return "directToScreen";
case kUpdateModeNormal:
default:
return "normal";
}
}
void QuickTimeDecoder::setUpdateMode(Common::String mode) {
if (mode.equalsIgnoreCase("updateBoth"))
_updateMode = kUpdateModeUpdateBoth;
else if (mode.equalsIgnoreCase("offscreenOnly"))
_updateMode = kUpdateModeOffscreenOnly;
else if (mode.equalsIgnoreCase("fromOffscreen"))
_updateMode = kUpdateModeFromOffscreen;
else if (mode.equalsIgnoreCase("directToScreen"))
_updateMode = kUpdateModeDirectToScreen;
else
_updateMode = kUpdateModeNormal;
warning("STUB: Update mode set to '%s'", getUpdateMode().c_str());
// normal Images are computed and displayed directly onto the screen
// while interactively panning and zooming. Provides the fastest
// overall frame rate, but individual frame draw times may be
// relatively slow for high-quality images on lower performance
// systems. Programmatic updates are displayed into the offscreen
// buffer, and then onto the screen.
//
// update Both Images are computed and displayed into the offscreen buffer,and
// then onto the screen. The use of the back buffer reduces the
// overall frame rate but provides the fastest imaging time for each
// frame.
//
// offscreenOnly Images are computed and displayed into the offscreen buffer
// only, and are not copied to the screen. Useful for preparing for a
// screen refresh that will have to happen quickly.
//
// fromOffscreen The offscreen buffer is copied directly to the screen. The offscreen
// buffer is refreshed only if it is out of date. The offscreen buffer is
// automatically updated if necessary to keep it in sync with the
// screen image.
//
// directToScreen Images are computed and displayed directly to the screen,
// without changing the offscreen buffer. Provides the fastest
// overall frame rate, but individual frame draw times may be
// relatively slow for high-quality images on lower performance
// systems.
((PanoTrackHandler *)getTrack(_panoTrack->targetTrack))->setDirty();
}
void QuickTimeDecoder::setTargetSize(uint16 w, uint16 h) {
if (!isVR())
warning("QuickTimeDecoder::setTargetSize() called on non-VR movie");
if (_qtvrType == QTVRType::PANORAMA) {
_width = w;
_height = h;
setFOV(_fov);
} else if (_qtvrType == QTVRType::OBJECT) {
if (_width != w)
_scaleFactorX *= Common::Rational(_width, w);
if (_height != h)
_scaleFactorY *= Common::Rational(_height, h);
_width = w;
_height = h;
}
// Set up the _hfov properly for the very first frame of the pano
// After our setFOV will handle the _hfov
_hfov = _fov * (float)_width / (float)_height;
}
void QuickTimeDecoder::setPanAngle(float angle) {
PanoSampleDesc *desc = (PanoSampleDesc *)_panoTrack->sampleDescs[0];
if (desc->_hPanStart != desc->_hPanStart && (desc->_hPanStart != 0.0 || desc->_hPanStart != 360.0)) {
if (angle < desc->_hPanStart) {
angle = desc->_hPanStart + _hfov / 2;
} else if (angle > desc->_hPanEnd - _hfov / 2) {
angle = desc->_hPanStart - _hfov / 2;
}
}
if (_panAngle != angle) {
PanoTrackHandler *track = (PanoTrackHandler *)getTrack(_panoTrack->targetTrack);
track->_currentPanAngle = _panAngle;
_panAngle = angle;
track->setDirty();
}
}
void QuickTimeDecoder::setTiltAngle(float angle) {
PanoSampleDesc *desc = (PanoSampleDesc *)_panoTrack->sampleDescs[0];
if (angle < desc->_vPanBottom + _fov / 2) {
angle = desc->_vPanBottom + _fov / 2;
} else if (angle > desc->_vPanTop - _fov / 2) {
angle = desc->_vPanTop - _fov / 2;
}
if (_tiltAngle != angle) {
PanoTrackHandler *track = (PanoTrackHandler *)getTrack(_panoTrack->targetTrack);
track->_currentTiltAngle = _tiltAngle;
_tiltAngle = angle;
track->setDirty();
}
}
bool QuickTimeDecoder::setFOV(float fov) {
PanoSampleDesc *desc = (PanoSampleDesc *)_panoTrack->sampleDescs[0];
bool success = true;
if (fov <= desc->_minimumZoom) {
fov = desc->_minimumZoom;
success = false;
} else if (fov >= desc->_maximumZoom) {
fov = desc->_maximumZoom;
success = false;
}
if (_fov != fov) {
PanoTrackHandler *track = (PanoTrackHandler *)getTrack(_panoTrack->targetTrack);
track->_currentFOV = _fov;
_fov = fov;
track->_currentHFOV = _hfov;
_hfov = _fov * (float)_width / (float)_height;
// We need to recalculate the pan angle and tilt angle to see if it has went
// out of bound for the current value of FOV
// This solves the distortion that we got sometimes when we zoom out at Tilt Angle != 0
setPanAngle(_panAngle);
setTiltAngle(_tiltAngle);
track->setDirty();
}
return success;
}
Common::String QuickTimeDecoder::getCurrentNodeName() {
if (_currentSample == -1)
return Common::String();
PanoTrackSample *sample = &_panoTrack->panoSamples[_currentSample];
return sample->strTable.getString(sample->hdr.nameStrOffset);
}
void QuickTimeDecoder::updateAngles() {
if (_qtvrType == QTVRType::OBJECT) {
_panAngle = (float)getCurrentColumn() / (float)_nav.columns * 360.0;
_tiltAngle = ((_nav.rows - 1) / 2.0 - (float)getCurrentRow()) / (float)(_nav.rows - 1) * 180.0;
debugC(1, kDebugLevelGVideo, "QTVR: row: %d col: %d (%d x %d) pan: %f tilt: %f", getCurrentRow(), getCurrentColumn(), _nav.rows, _nav.columns, getPanAngle(), getTiltAngle());
}
}
void QuickTimeDecoder::setCurrentRow(int row) {
VideoTrackHandler *track = (VideoTrackHandler *)_nextVideoTrack;
int currentColumn = track->getCurFrame() % _nav.columns;
int newFrame = row * _nav.columns + currentColumn;
if (newFrame >= 0 && newFrame < track->getFrameCount()) {
track->setCurFrame(newFrame);
}
}
void QuickTimeDecoder::setCurrentColumn(int column) {
VideoTrackHandler *track = (VideoTrackHandler *)_nextVideoTrack;
int currentRow = track->getCurFrame() / _nav.columns;
int newFrame = currentRow * _nav.columns + column;
if (newFrame >= 0 && newFrame < track->getFrameCount()) {
track->setCurFrame(newFrame);
}
}
void QuickTimeDecoder::nudge(const Common::String &direction) {
VideoTrackHandler *track = (VideoTrackHandler *)_nextVideoTrack;
int curFrame = track->getCurFrame();
int currentRow = curFrame / _nav.columns;
int currentRowStart = currentRow * _nav.columns;
int newFrame = curFrame;
if (direction.equalsIgnoreCase("left")) {
newFrame = (curFrame - 1 - currentRowStart) % _nav.columns + currentRowStart;
} else if (direction.equalsIgnoreCase("right")) {
newFrame = (curFrame + 1 - currentRowStart) % _nav.columns + currentRowStart;
} else if (direction.equalsIgnoreCase("up")) {
newFrame = curFrame - _nav.columns;
if (newFrame < 0)
return;
} else if (direction.equalsIgnoreCase("down")) {
newFrame = curFrame + _nav.columns;
if (newFrame >= track->getFrameCount())
return;
} else {
error("QuickTimeDecoder::nudge(): Invald direction: ('%s')!", direction.c_str());
}
track->setCurFrame(newFrame);
}
QuickTimeDecoder::NodeData QuickTimeDecoder::getNodeData(uint32 nodeID) {
for (const auto &sample : _panoTrack->panoSamples) {
if (sample.hdr.nodeID == nodeID) {
return {
nodeID,
sample.hdr.defHPan,
sample.hdr.defVPan,
sample.hdr.defZoom,
sample.hdr.minHPan,
sample.hdr.minVPan,
sample.hdr.maxHPan,
sample.hdr.maxVPan,
sample.hdr.minZoom,
sample.strTable.getString(sample.hdr.nameStrOffset)};
}
}
error("QuickTimeDecoder::getNodeData(): Node with nodeID %d not found!", nodeID);
return {};
}
void QuickTimeDecoder::goToNode(uint32 nodeID) {
int idx = -1;
for (uint i = 0; i < _panoTrack->panoSamples.size(); i++) {
if (_panoTrack->panoSamples[i].hdr.nodeID == nodeID) {
idx = i;
break;
}
}
if (idx == -1) {
warning("QuickTimeDecoder::goToNode(): Incorrect nodeID: %d", nodeID);
return;
}
_currentSample = idx;
setPanAngle(_panoTrack->panoSamples[_currentSample].hdr.defHPan);
setTiltAngle(_panoTrack->panoSamples[_currentSample].hdr.defVPan);
setFOV(_panoTrack->panoSamples[_currentSample].hdr.defZoom);
((PanoTrackHandler *)getTrack(_panoTrack->targetTrack))->constructPanorama();
}
/////////////////////////
// PANO Track
////////////////////////
QuickTimeDecoder::PanoTrackHandler::PanoTrackHandler(QuickTimeDecoder *decoder, Common::QuickTimeParser::Track *parent) : _decoder(decoder), _parent(parent) {
if (decoder->_qtvrType != QTVRType::PANORAMA)
error("QuickTimeDecoder::PanoTrackHandler: Incorrect track passed");
_isPanoConstructed = false;
_constructedPano = nullptr;
_constructedHotspots = nullptr;
_upscaledConstructedPano = nullptr;
_projectedPano = nullptr;
_planarProjection = nullptr;
_dirty = true;
_decoder->updateQTVRCursor(0, 0); // Initialize all things for cursor
}
QuickTimeDecoder::PanoTrackHandler::~PanoTrackHandler() {
if (_isPanoConstructed) {
_constructedPano->free();
delete _constructedPano;
_upscaledConstructedPano->free();
delete _upscaledConstructedPano;
_constructedHotspots->free();
delete _constructedHotspots;
}
if (_projectedPano) {
_projectedPano->free();
delete _projectedPano;
_planarProjection->free();
delete _planarProjection;
}
}
uint16 QuickTimeDecoder::PanoTrackHandler::getWidth() const {
return getScaledWidth().toInt();
}
uint16 QuickTimeDecoder::PanoTrackHandler::getHeight() const {
return getScaledHeight().toInt();
}
Graphics::PixelFormat QuickTimeDecoder::PanoTrackHandler::getPixelFormat() const {
PanoSampleDesc *desc = (PanoSampleDesc *)_parent->sampleDescs[0];
VideoTrackHandler *track = (VideoTrackHandler *)(_decoder->getTrack(_decoder->Common::QuickTimeParser::_tracks[desc->_sceneTrackID - 1]->targetTrack));
return track->getPixelFormat();
}
Common::Rational QuickTimeDecoder::PanoTrackHandler::getScaledWidth() const {
return Common::Rational(_parent->width) / _parent->scaleFactorX;
}
Common::Rational QuickTimeDecoder::PanoTrackHandler::getScaledHeight() const {
return Common::Rational(_parent->height) / _parent->scaleFactorY;
}
void QuickTimeDecoder::PanoTrackHandler::swingTransitionHandler() {
// The number of steps (the speed) of the transition is decided by the _transitionSpeed
const int NUM_STEPS = 300 / _decoder->_transitionSpeed;
// Calculate the step
float stepFOV = (_decoder->_fov - _currentFOV) / NUM_STEPS;
float stepHFOV = (_decoder->_hfov - _currentHFOV) / NUM_STEPS;
float stepTiltAngle = (_decoder->_tiltAngle - _currentTiltAngle) / NUM_STEPS;
float targetPanAngle = _decoder->_panAngle;
float difference = ABS(_currentPanAngle - targetPanAngle);
float stepPanAngle;
// All of this is just because the panorama is supposed to take the smallest path
if (targetPanAngle > _currentPanAngle) {
if (difference <= 180) {
stepPanAngle = difference / NUM_STEPS;
} else {
stepPanAngle = (- (360 - difference)) / NUM_STEPS;
}
} else {
if (difference <= 180) {
stepPanAngle = (- difference) / NUM_STEPS;
} else {
stepPanAngle = (360 - difference) / NUM_STEPS;
}
}
int16 rectLeft = _decoder->_origin.x;
int16 rectRight = _decoder->_origin.y;
Common::Point mouse;
int mw = _decoder->_width, mh = _decoder->_height;
for (int i = 0; i < NUM_STEPS; i++) {
projectPanorama(1,
_currentFOV + i * stepFOV,
_currentHFOV + i * stepHFOV,
_currentTiltAngle + i * stepTiltAngle,
_currentPanAngle + i * stepPanAngle);
g_system->copyRectToScreen(_projectedPano->getPixels(),
_projectedPano->pitch,
rectLeft,
rectRight,
_projectedPano->w,
_projectedPano->h);
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
if (event.type == Common::EVENT_QUIT) {
debugC(1, kDebugLevelGVideo, "EVENT_QUIT passed during the Swing Transition.");
g_system->quit();
return;
}
if (Common::isMouseEvent(event)) {
mouse = event.mouse;
}
// Referenced from video.cpp in testbed engine
if (mouse.x >= _decoder->_prevMouse.x &&
mouse.x < _decoder->_prevMouse.x + mw &&
mouse.y >= _decoder->_prevMouse.y &&
mouse.y < _decoder->_prevMouse.y + mh) {
switch (event.type) {
case Common::EVENT_MOUSEMOVE:
_decoder->handleMouseMove(event.mouse.x - _decoder->_prevMouse.x, event.mouse.y - _decoder->_prevMouse.y);
break;
default:
break;
}
}
}
g_system->updateScreen();
g_system->delayMillis(10);
}
// This is so if we call swingTransitionHandler() twice
// The second time there will be no transition
_currentFOV = _decoder->_fov;
_currentPanAngle = _decoder->_panAngle;
_currentHFOV = _decoder->_hfov;
_currentTiltAngle = _decoder->_tiltAngle;
// Due to floating point errors, we may end a few degrees here and there
// Make sure we reach the destination at the end of this loop
// Also we have to go back to our original quality
projectPanorama(3, _decoder->_fov, _decoder->_hfov, _decoder->_panAngle, _decoder->_panAngle);
}
const Graphics::Surface *QuickTimeDecoder::PanoTrackHandler::decodeNextFrame() {
if (!_isPanoConstructed)
return nullptr;
if (_dirty && _decoder->_transitionMode == kTransitionModeNormal) {
float quality = _decoder->getQuality();
float fov = _decoder->_fov;
float hfov = _decoder->_hfov;
float tiltAngle = _decoder->_tiltAngle;
float panAngle = _decoder->_panAngle;
switch ((int)quality) {
case 1:
projectPanorama(1, fov, hfov, tiltAngle, panAngle);
break;
case 2:
projectPanorama(2, fov, hfov, tiltAngle, panAngle);
break;
case 4:
projectPanorama(3, fov, hfov, tiltAngle, panAngle);
break;
case 0:
if (_decoder->_isMouseButtonDown || _decoder->_isKeyDown) {
projectPanorama(1, fov, hfov, tiltAngle, panAngle);
} else {
projectPanorama(3, fov, hfov, tiltAngle, panAngle);
}
break;
default:
projectPanorama(3, fov, hfov, tiltAngle, panAngle);
break;
}
} else if (_decoder->_transitionMode == kTransitionModeSwing) {
swingTransitionHandler();
}
return _projectedPano;
}
const Graphics::Surface *QuickTimeDecoder::PanoTrackHandler::bufferNextFrame() {
return nullptr;
}
Graphics::Surface *QuickTimeDecoder::PanoTrackHandler::constructMosaic(VideoTrackHandler *track, uint w, uint h, Common::String fname) {
int16 framew = track->getWidth();
int16 frameh = track->getHeight();
Graphics::Surface *target = new Graphics::Surface();
target->create(w * framew, h * frameh, track->getPixelFormat());
debugC(1, kDebugLevelGVideo, "Pixel format: %s", track->getPixelFormat().toString().c_str());
Common::Rect srcRect(0, 0, framew, frameh);
for (uint y = 0; y < h; y++) {
for (uint x = 0; x < w; x++) {
const Graphics::Surface *frame = track->bufferNextFrame();
if (!frame) {
warning("QuickTimeDecoder::PanoTrackHandler::constructMosaic(): Out of frames at: %d, %d", x, y);
break;
}
target->copyRectToSurface(*frame, x * framew, y * frameh, srcRect);
}
}
if (ConfMan.getBool("dump_scripts")) {
Common::Path path = Common::Path(fname);
Common::DumpFile bitmapFile;
if (!bitmapFile.open(path, true)) {
warning("Cannot dump panorama into file '%s'", path.toString().c_str());
target->free();
delete target;
return nullptr;
}
Image::writePNG(bitmapFile, *target, track->getPalette());
bitmapFile.close();
debug(0, "Dumped panorama %s of %d x %d", path.toString().c_str(), target->w, target->h);
}
return target;
}
void QuickTimeDecoder::PanoTrackHandler::initPanorama() {
_decoder->goToNode(_decoder->_panoTrack->panoInfo.defNodeID);
}
Graphics::Surface *QuickTimeDecoder::PanoTrackHandler::upscalePanorama(Graphics::Surface *sourceSurface, int8 level) {
// This algorithm (bilinear upscaling) takes quite some time to complete
//
// upscaling method
//
// for level 1 upscaling
// a | b a | avg(a, b) | b
// ----- => -----------------
// c | d c | avg(c, d) | d
//
// for level 2 upscaling
// a | avg(a, b) | b
// a | b -----------------------------------------
// ----- => avg(a, c) [e] | avg(e, f) | avg(b, d) [f]
// c | d -----------------------------------------
// c | avg(c, d) | d
uint w = sourceSurface->w;
uint h = sourceSurface->h;
Graphics::Surface *target = new Graphics::Surface();
if (level == 2) {
target->create(w * 2, h * 2, sourceSurface->format);
for (uint y = 0; y < h; ++y) {
for (uint x = 0; x < w; ++x) {
// For the row x==w and column y==h, wrap around the panorama
// and average these pixels with row x==0 and column y==0 respectively
uint x1 = (x + 1) % w;
uint y1 = (y + 1) % h;
// Couldn't find a better way to do this
// Should I write a function in the Surface or PixelFormat class?
int32 p00 = sourceSurface->getPixel(x, y);
int32 p01 = sourceSurface->getPixel(x, y1);
int32 p10 = sourceSurface->getPixel(x1, y);
int32 p11 = sourceSurface->getPixel(x1, y1);
uint8 a00, r00, g00, b00;
uint8 a01, r01, g01, b01;
uint8 a10, r10, g10, b10;
uint8 a11, r11, g11, b11;
sourceSurface->format.colorToARGB(p00, a00, r00, g00, b00);
sourceSurface->format.colorToARGB(p01, a01, r01, g01, b01);
sourceSurface->format.colorToARGB(p10, a10, r10, g10, b10);
sourceSurface->format.colorToARGB(p11, a11, r11, g11, b11);
int32 avgPixel00 = p00;
int32 avgPixel01 = sourceSurface->format.ARGBToColor((a00 + a01) >> 1, (r00 + r01) >> 1, (g00 + g01) >> 1, (b00 + b01) >> 1);
int32 avgPixel10 = sourceSurface->format.ARGBToColor((a00 + a10) >> 1, (r00 + r10) >> 1, (g00 + g10) >> 1, (b00 + b10) >> 1);
int32 avgPixel11 = sourceSurface->format.ARGBToColor(
(a00 + a01 + a10 + a11) >> 2,
(r00 + r01 + r10 + r11) >> 2,
(g00 + g01 + g10 + g11) >> 2,
(b00 + b01 + b10 + b11) >> 2);
target->setPixel(x * 2, y * 2, avgPixel00);
target->setPixel(x * 2, y * 2 + 1, avgPixel01);
target->setPixel(x * 2 + 1, y * 2, avgPixel10);
target->setPixel(x * 2 + 1, y * 2 + 1,avgPixel11);
}
}
} else {
target->create(w * 2, h, sourceSurface->format);
for (uint y = 0; y < h; ++y) {
for (uint x = 0; x < w; ++x) {
uint x1 = (x + 1) % w;
int32 p00 = sourceSurface->getPixel(x, y);
int32 p01 = sourceSurface->getPixel(x1, y);
uint8 a00, r00, g00, b00;
uint8 a01, r01, g01, b01;
sourceSurface->format.colorToARGB(p00, a00, r00, g00, b00);
sourceSurface->format.colorToARGB(p01, a01, r01, g01, b01);
int32 avgPixel00 = p00;
int32 avgPixel01 = sourceSurface->format.ARGBToColor((a00 + a01) >> 1, (r00 + r01) >> 1, (g00 + g01) >> 1, (b00 + b01) >> 1);
target->setPixel(x * 2, y, avgPixel00);
target->setPixel(x * 2 + 1, y, avgPixel01);
}
}
}
return target;
}
void QuickTimeDecoder::PanoTrackHandler::boxAverage(Graphics::Surface *sourceSurface, uint8 scaleFactor) {
uint16 h = sourceSurface->h;
uint16 w = sourceSurface->w;
// Apply box average if quality is higher than 1
// Otherwise it'll be quicker to just copy the _planarProjection onto _projectedPano
if (scaleFactor == 1) {
_projectedPano->copyFrom(*sourceSurface);
return;
}
uint8 scaleSquare = scaleFactor * scaleFactor;
// Average out the pixels from the larger image
for (uint16 y = 0; y < h / scaleFactor; y++) {
for (uint16 x = 0; x < w / scaleFactor; x++) {
uint16 avgA = 0, avgR = 0, avgG = 0, avgB = 0;
for (uint8 row = 0; row < scaleFactor; row++) {
for (uint8 column = 0; column < scaleFactor; column++) {
uint8 a00, r00, g00, b00;
uint32 pixel00 = sourceSurface->getPixel(MIN((x * scaleFactor + row), w - 1), MIN((y * scaleFactor + column), h - 1));
_projectedPano->format.colorToARGB(pixel00, a00, r00, g00, b00);
avgA += a00;
avgR += r00;
avgG += g00;
avgB += b00;
}
}
avgA /= scaleSquare;
avgR /= scaleSquare;
avgG /= scaleSquare;
avgB /= scaleSquare;
uint32 avgPixel = _projectedPano->format.ARGBToColor(avgA, avgR, avgG, avgB);
_projectedPano->setPixel(x, y, avgPixel);
}
}
}
void QuickTimeDecoder::PanoTrackHandler::constructPanorama() {
PanoSampleDesc *desc = (PanoSampleDesc *)_parent->sampleDescs[0];
PanoTrackSample *sample = &_parent->panoSamples[_decoder->_currentSample];
if (_constructedPano) {
_constructedPano->free();
delete _constructedPano;
_constructedHotspots->free();
delete _constructedHotspots;
}
debugC(1, kDebugLevelGVideo, "scene: %d (%d x %d) hotspots: %d (%d x %d)", desc->_sceneTrackID, desc->_sceneSizeX, desc->_sceneSizeY,
desc->_hotSpotTrackID, desc->_hotSpotSizeX, desc->_hotSpotSizeY);
debugC(1, kDebugLevelGVideo, "sceneNumFrames: %d x %d sceneColorDepth: %d", desc->_sceneNumFramesX, desc->_sceneNumFramesY, desc->_sceneColorDepth);
debugC(1, kDebugLevelGVideo, "Node idx: %d", sample->hdr.nodeID);
int nodeidx = -1;
for (int i = 0; i < (int)_parent->panoInfo.nodes.size(); i++)
if (_parent->panoInfo.nodes[i].nodeID == sample->hdr.nodeID) {
nodeidx = i;
break;
}
if (nodeidx == -1) {
warning("constructPanorama(): Missing node %d in anoInfo", sample->hdr.nodeID);
nodeidx = 0;
}
uint32 timestamp = _parent->panoInfo.nodes[nodeidx].timestamp;
debugC(1, kDebugLevelGVideo, "Timestamp: %d", timestamp);
VideoTrackHandler *track = (VideoTrackHandler *)(_decoder->getTrack(_decoder->Common::QuickTimeParser::_tracks[desc->_sceneTrackID - 1]->targetTrack));
track->seek(Audio::Timestamp(0, timestamp, _decoder->_timeScale));
_constructedPano = constructMosaic(track, desc->_sceneNumFramesX, desc->_sceneNumFramesY, "dumps/pano-full.png");
track = (VideoTrackHandler *)(_decoder->getTrack(_decoder->Common::QuickTimeParser::_tracks[desc->_hotSpotTrackID - 1]->targetTrack));
track->seek(Audio::Timestamp(0, timestamp, _decoder->_timeScale));
_constructedHotspots = constructMosaic(track, desc->_hotSpotNumFramesX, desc->_hotSpotNumFramesY, "dumps/pano-hotspot.png");
_isPanoConstructed = true;
}
Common::Point QuickTimeDecoder::PanoTrackHandler::projectPoint(int16 mx, int16 my) {
if (!_isPanoConstructed)
return Common::Point(-1, -1);
uint16 w = _decoder->getWidth(), h = _decoder->getHeight();
uint16 hotWidth = _constructedHotspots->w, hotHeight = _constructedHotspots->h;
PanoSampleDesc *desc = (PanoSampleDesc *)_parent->sampleDescs[0];
float cornerVectors[3][3];
float *topRightVector = cornerVectors[0];
float *bottomRightVector = cornerVectors[1];
float *mousePixelVector = cornerVectors[2];
bottomRightVector[1] = tan(_decoder->_fov * M_PI / 360.0);
bottomRightVector[0] = bottomRightVector[1] * (float)w / (float)h;
bottomRightVector[2] = 1.0f;
topRightVector[0] = bottomRightVector[0];
topRightVector[1] = -bottomRightVector[1];
topRightVector[2] = bottomRightVector[2];
// Apply pitch (tilt) rotation
float cosTilt = cos(-_decoder->_tiltAngle * M_PI / 180.0);
float sinTilt = sin(-_decoder->_tiltAngle * M_PI / 180.0);
for (int v = 0; v < 2; v++) {
float y = cornerVectors[v][1];
float z = cornerVectors[v][2];
float newZ = z * cosTilt - y * sinTilt;
float newY = y * cosTilt + z * sinTilt;
cornerVectors[v][1] = newY;
cornerVectors[v][2] = newZ;
}
float minTiltY = tan(desc->_vPanBottom * M_PI / 180.0f);
float maxTiltY = tan(desc->_vPanTop * M_PI / 180.0f);
int warpMode = _decoder->_warpMode;
int hotX, hotY;
// Compute the largest projected X value, which determines the horizontal angle range
float maxProjectedX = 0.0f;
// X coords are the same here so whichever has the lower Z coord will have the maximum projected X
if (topRightVector[2] < bottomRightVector[2])
maxProjectedX = topRightVector[0] / topRightVector[2];
else
maxProjectedX = bottomRightVector[0] / bottomRightVector[2];
// Compute the side edge vector by interpolating between topRightVector and
// bottomRightVector based on the mouse Y position
float yRatio = (float)my / (float)h;
mousePixelVector[0] = topRightVector[0] + yRatio * (bottomRightVector[0] - topRightVector[0]);
mousePixelVector[1] = topRightVector[1] + yRatio * (bottomRightVector[1] - topRightVector[1]);
mousePixelVector[2] = topRightVector[2] + yRatio * (bottomRightVector[2] - topRightVector[2]);
float t, xCoord = 0, yawRatio = 0;
float horizontalFovRadians = _decoder->_hfov * M_PI / 180.0f;
float verticalFovRadians = _decoder->_fov * M_PI / 180.0f;
float tiltAngleRadians = -_decoder->_tiltAngle * M_PI / 180.0f;
if (warpMode == 0) {
t = (float)mx / (float)(w) - 0.5f;
xCoord = t * horizontalFovRadians / M_PI / 2.0f;
yawRatio = xCoord;
} else {
t = ((float)(mx - w / 2) / (float)w * 2.0);
if (warpMode == 1) {
xCoord = t * maxProjectedX;
} else if (warpMode == 2) {
xCoord = t * mousePixelVector[0] / mousePixelVector[2];
}
yawRatio = atan(xCoord) / (2.0 * M_PI);
}
float angleT = (360.0f - _decoder->_panAngle) / 360.0f;
// panorama is turned 90 degrees, width is height
hotX = (1.0f - (angleT + yawRatio)) * (float)hotHeight;
if (warpMode == 0) {
float tiltFactor = tan(verticalFovRadians / 2.0f);
float normalizedY = ((float)my / (float)(h - 1)) * 2.0f - 1.0f; // Range [-1, 1]
float projectedY = (normalizedY * tiltFactor) + tan(tiltAngleRadians);
hotY = static_cast<int32>((projectedY + 1.0f) / 2.0f * hotWidth);
} else {
// To get the vertical coordinate, need to project the vector on to a unit cylinder.
// To do that, compute the length of the XZ vector,
float xzVectorLen = sqrt(xCoord * xCoord + 1.0f);
float projectedY = xzVectorLen * mousePixelVector[1] / mousePixelVector[2];
float normalizedYCoordinate = (projectedY - minTiltY) / (maxTiltY - minTiltY);
hotY = (int)(normalizedYCoordinate * (float)hotWidth);
}
hotX = hotX % hotHeight;
if (hotX < 0)
hotX += hotHeight;
if (hotY < 0)
hotY = 0;
else if (hotY > hotWidth)
hotY = hotWidth;
return Common::Point(hotX, hotY);
}
// It may get confusing what the three terms _quality, scaleVector and _upscaleLevel mean
// They are all related to the quality of the _projectedPanorama
// These are relevant for the functions upscalePanorama(), boxAverage() and projectPanorama()
//
// _quality is the attribute of the panorama that dictates how good the panorama looks
// This is taken directly from the original QTVR Xtra documentation by Apple
// (float) _quality can take three values:
// 1.0f; (higher
// 2.0f; means
// 4.0f; better)
// Excluding _quality = 0.0f (which is a combination of _quality = 1.0f and _quality = 4.0f)
// See setQuality() function to get a better idea
//
// This _quality determines what the scaleFactor value will be passed to projectPanorama()
// scaleFactor is the parameter to projectPanorama used for anti-aliasing the panorama
// We project a Surface object that has [scaleFactor] times the target height and width from _constructedPano
// Then we take a boxAverage() in a [scaleFactor * scaleFactor] box to make the panorama target sized
// This effectively removes aliasing from the image
// (uint8) scaleFactor can take three values:
// 1; (no-anti-aliasing)
// 2; (some anti-aliasing)
// 3; (better anti-aliasing) (For better Performance, we may remove this option)
//
// _quality also controls another factor that is the scale of original panorama (_constructedPano)
// Basically we upscalePanorama() at higher _quality values, exactly by how much is given by _upscaleLevel
// (uint8) _upscaleLevel can take two values:
// 1 (upscale to twice the width);
// 2 (upscale to twice the width and height);
//
// _quality = 1.0f => scaleFactor = 1; No upscaling;
// _quality = 2.0f => scaleFactor = 2; _upscaleLevel = 1;
// _quality = 4.0f => scaleFactor = 3; _upscaleLevel = 2;
void QuickTimeDecoder::PanoTrackHandler::projectPanorama(uint8 scaleFactor,
float fov,
float hfov,
float tiltAngle,
float panAngle) {
if (!_isPanoConstructed)
return;
uint16 w = _decoder->getWidth() * scaleFactor, h = _decoder->getHeight() * scaleFactor;
if (!_projectedPano) {
if (w == 0 || h == 0)
error("QuickTimeDecoder::PanoTrackHandler::projectPanorama(): setTargetSize() was not called");
_projectedPano = new Graphics::Surface();
_projectedPano->create(w / scaleFactor, h / scaleFactor, _constructedPano->format);
}
// The size of _planarProjection will keep changing in quality mode 0
// It might (will definitely) cause some out of bound access if left as it is
if (!_planarProjection || _planarProjection->w != w || _planarProjection->h != h) {
// If _planarProjection holds some pixels, first need to free all the pixel data
if (_planarProjection) {
_planarProjection->free();
}
delete _planarProjection;
_planarProjection = new Graphics::Surface();
_planarProjection->create(w, h, _constructedPano->format);
}
PanoSampleDesc *desc = (PanoSampleDesc *)_parent->sampleDescs[0];
float cornerVectors[2][3];
float *topRightVector = cornerVectors[0];
float *bottomRightVector = cornerVectors[1];
// tangent of half of fov angle, is the bottom edge point
// the negative of that is the top edge point
bottomRightVector[1] = tan(fov * M_PI / 360.0);
bottomRightVector[0] = bottomRightVector[1] * (float)w / (float)h;
bottomRightVector[2] = 1.0f;
topRightVector[0] = bottomRightVector[0];
topRightVector[1] = -bottomRightVector[1];
topRightVector[2] = bottomRightVector[2];
// Apply pitch (tilt) rotation
float cosTilt = cos(-tiltAngle * M_PI / 180.0);
float sinTilt = sin(-tiltAngle * M_PI / 180.0);
// Using trigonometry to account for the tilt
for (int v = 0; v < 2; v++) {
float y = cornerVectors[v][1];
float z = cornerVectors[v][2];
float newZ = z * cosTilt - y * sinTilt;
float newY = y * cosTilt + z * sinTilt;
cornerVectors[v][1] = newY;
cornerVectors[v][2] = newZ;
}
float minTiltY = tan(desc->_vPanBottom * M_PI / 180.0f);
float maxTiltY = tan(desc->_vPanTop * M_PI / 180.0f);
// Compute the largest projected X value, which determines the horizontal angle range
// Same with max projected y as well
float maxProjectedX = 0.0f;
// X coords are the same here so whichever has the lower Z coord will have the maximum projected X
if (topRightVector[2] < bottomRightVector[2])
maxProjectedX = topRightVector[0] / topRightVector[2];
else
maxProjectedX = bottomRightVector[0] / bottomRightVector[2];
float minProjectedY = topRightVector[1] / topRightVector[2];
float maxProjectedY = bottomRightVector[1] / bottomRightVector[2];
float angleT = fmod((360.0f - panAngle) / 360.0f, 1.0f);
if (angleT < 0.0f) {
angleT += 1.0f;
}
Graphics::Surface *sourceSurface;
switch (scaleFactor) {
case 1:
sourceSurface = _decoder->_renderHotspots ? _constructedHotspots : _constructedPano;
break;
case 2:
if (!_upscaledConstructedPano || _upscaleLevel != 1) {
// Avoid memory leakage if _upscaledConstructedPano already points to a Panorama
if (_upscaledConstructedPano) {
_upscaledConstructedPano->free();
delete _upscaledConstructedPano;
}
_upscaleLevel = 1;
_upscaledConstructedPano = upscalePanorama(_constructedPano, _upscaleLevel);
}
sourceSurface = _decoder->_renderHotspots ? _constructedHotspots : _upscaledConstructedPano;
break;
case 3:
if (!_upscaledConstructedPano || _upscaleLevel != 2) {
// Avoid memory leakage if _upscaledConstructedPano already points to a Panorama
if (_upscaledConstructedPano) {
_upscaledConstructedPano->free();
delete _upscaledConstructedPano;
}
_upscaleLevel = 2;
_upscaledConstructedPano = upscalePanorama(_constructedPano, _upscaleLevel);
}
sourceSurface = _decoder->_renderHotspots ? _constructedHotspots : _upscaledConstructedPano;
break;
default:
sourceSurface = _decoder->_renderHotspots ? _constructedHotspots : _constructedPano;
break;
}
int32 panoWidth = sourceSurface->h;
int32 panoHeight = sourceSurface->w;
// This angle offset will tell you exactly which portion of Cylindrical panorama
// (when you construct a rectangular mosaic out of it) you're projecting on the plane surface
uint16 angleOffset = static_cast<uint32>(angleT * panoWidth);
const bool isWidthOdd = ((w % 2) == 1);
uint16 halfWidthRoundedUp = (w + 1) / 2;
float halfWidthFloat = (float)w * 0.5f;
float verticalFovRadians = fov * M_PI / 180.0f;
float horizontalFovRadians = hfov * M_PI / 180.0f;
float tiltAngleRadians = tiltAngle * M_PI / 180.0f;
float warpMode = _decoder->_warpMode;
Common::Array<float> cylinderProjectionRanges;
Common::Array<float> cylinderAngleOffsets;
cylinderProjectionRanges.resize(halfWidthRoundedUp * 2);
cylinderAngleOffsets.resize(halfWidthRoundedUp);
if (warpMode == 0.0f) {
for (uint16 x = 0; x < halfWidthRoundedUp; x++) {
float xFloat = (float) x;
if (!isWidthOdd) {
xFloat += 0.5f;
}
float normalizedX = (float)xFloat / (float)(w - 1);
cylinderAngleOffsets[x] = normalizedX * horizontalFovRadians / M_PI / 2.0f; // Scale to FOV
}
} else {
for (uint16 x = 0; x < halfWidthRoundedUp; x++) {
float xFloat = (float)x;
// If width is odd, then the first column is on the pixel center
// If width is even, then the first column is on the pixel boundary
if (!isWidthOdd) {
xFloat += 0.5f;
}
float t = xFloat / halfWidthFloat;
float xCoord = t * maxProjectedX;
float yCoords[2] = {minProjectedY, maxProjectedY};
float length = sqrt(xCoord * xCoord + 1.0f);
// Compute projection ranges
for (int v = 0; v < 2; v++) {
// Intersect (xCoord, yCoord[v], 1) with a 1-radius cylinder
float newY = yCoords[v] / length;
cylinderProjectionRanges[x * 2 + v] = (newY - minTiltY) / (maxTiltY - minTiltY);
}
cylinderAngleOffsets[x] = atan(xCoord) * 0.5f / M_PI;
}
}
for (uint16 x = 0; x < halfWidthRoundedUp; x++) {
int32 centerXImageCoord = static_cast<int32>(angleOffset);
int32 edgeCoordOffset = static_cast<int32>(cylinderAngleOffsets[x] * panoWidth);
int32 leftSourceXCoord = centerXImageCoord - edgeCoordOffset;
int32 rightSourceXCoord = centerXImageCoord + edgeCoordOffset;
int32 topSrcCoord = 0, bottomSrcCoord = 0;
if (warpMode != 0) {
topSrcCoord = static_cast<int32>(cylinderProjectionRanges[x * 2 + 0] * panoHeight);
bottomSrcCoord = static_cast<int32>(cylinderProjectionRanges[x * 2 + 1] * panoHeight);
if (topSrcCoord < 0) {
topSrcCoord = 0;
} else if (topSrcCoord >= panoHeight) {
topSrcCoord = panoHeight - 1;
}
if (bottomSrcCoord >= panoHeight) {
bottomSrcCoord = panoHeight - 1;
}
}
// The descriptor has the original sceneSizeX and sceneSizeY of the movie
// But in quality mode 3 we're increasing them to twice the original
// This factor just make sure we're wrapping leftSourceXCoord and rightSourceXCoord
// According to the sourceSurface's dimensions rather than the original's
int factor = scaleFactor == 3 && !_decoder->_renderHotspots ? 2 : 1;
// Wrap around if out of range
leftSourceXCoord = leftSourceXCoord % static_cast<int32>(panoWidth);
if (leftSourceXCoord < 0) {
leftSourceXCoord += panoWidth;
}
leftSourceXCoord = desc->_sceneSizeY * factor - 1 - leftSourceXCoord;
rightSourceXCoord = rightSourceXCoord % static_cast<int32>(panoWidth);
if (rightSourceXCoord < 0) {
rightSourceXCoord += panoWidth;
}
rightSourceXCoord = desc->_sceneSizeY * factor - 1 - rightSourceXCoord;
uint16 x1 = halfWidthRoundedUp - 1 - x;
uint16 x2 = w - halfWidthRoundedUp + x;
for (uint16 y = 0; y < h; y++) {
int32 sourceYCoord;
if (warpMode == 0) {
float tiltFactor = tan(verticalFovRadians / 2.0f);
float normalizedY = ((float)y / (float)(h - 1)) * 2.0f - 1.0f;
float projectedY = (normalizedY * tiltFactor) - tan(tiltAngleRadians);
sourceYCoord = static_cast<int32>((projectedY + 1.0f) / 2.0f * panoHeight);
} else {
sourceYCoord = (2 * y + 1) * (bottomSrcCoord - topSrcCoord) / (2 * h) + topSrcCoord;
}
if (sourceYCoord < 0)
sourceYCoord = 0;
if (sourceYCoord >= panoHeight)
sourceYCoord = panoHeight - 1;
// Our panorma is apparently vertical, that's why we're passing Y co-ord before X co-ord
uint32 pixel1 = sourceSurface->getPixel(sourceYCoord, leftSourceXCoord);
uint32 pixel2 = sourceSurface->getPixel(sourceYCoord, rightSourceXCoord);
if (_decoder->_renderHotspots) {
const byte *col1 = &quickTimeDefaultPalette256[pixel1 * 3];
pixel1 = _planarProjection->format.RGBToColor(col1[0], col1[1], col1[2]);
const byte *col2 = &quickTimeDefaultPalette256[pixel2 * 3];
pixel2 = _planarProjection->format.RGBToColor(col2[0], col2[1], col2[2]);
}
_planarProjection->setPixel(x1, y, pixel1);
_planarProjection->setPixel(x2, y, pixel2);
}
}
if (warpMode != 2) {
boxAverage(_planarProjection, scaleFactor);
_dirty = false;
return;
}
// If warp mode is set to 2, also apply the perspective projection
Common::Array<float> sideEdgeXYInterpolators;
// The X interpolators are from 0 to maxProjectedX
// The Y interpolators are the interpolator from minProjectedY to maxProjectedY
sideEdgeXYInterpolators.resize(h * 2);
for (uint16 y = 0; y < h; y++) {
float t = ((float)y + 0.5f) / (float)h;
float vector[3];
for (int v = 0; v < 3; v++) {
vector[v] = cornerVectors[0][v] * (1.0f - t) + cornerVectors[1][v] * t;
}
float projectedX = vector[0] / vector[2];
float projectedY = vector[1] / vector[2];
sideEdgeXYInterpolators[y * 2 + 0] = projectedX / maxProjectedX;
sideEdgeXYInterpolators[y * 2 + 1] = (projectedY - minProjectedY) / (maxProjectedY - minProjectedY);
}
Graphics::Surface *aliasedProjectedPano = new Graphics::Surface();
aliasedProjectedPano->create(w, h, _planarProjection->format);
// Convert planar projection into perspective projection
for (uint16 y = 0; y < h; y++) {
float xInterpolator = sideEdgeXYInterpolators[y * 2 + 0];
float yInterpolator = sideEdgeXYInterpolators[y * 2 + 1];
int32 srcY = static_cast<int32>(yInterpolator * (float)h);
int32 scanlineWidth = static_cast<int32>(xInterpolator * w);
int32 startX = (w - scanlineWidth) / 2;
//int32 endX = startX + scanlineWidth;
// It would be better to compute a reciprocal and do this as a multiply instead,
// doing divides per pixel is SLOW!
for (uint16 x = 0; x < w; x++) {
int32 srcX = (x + 0.5f) * xInterpolator + startX;
uint32 pixel = _planarProjection->getPixel(srcX, srcY);
// I increased the size of the surface (made it twice as wide and twice as tall)
// Now I'm just getting the perspective projected pixel and creating a 2x2 box
aliasedProjectedPano->setPixel(x, y, pixel);
}
}
boxAverage(aliasedProjectedPano, scaleFactor);
// Memory leakage avoided; Should I use a stack allocated object?
aliasedProjectedPano->free();
delete aliasedProjectedPano;
_dirty = false;
}
Common::Point QuickTimeDecoder::getPanLoc(int16 x, int16 y) {
PanoTrackHandler *track = (PanoTrackHandler *)getTrack(_panoTrack->targetTrack);
return track->projectPoint(x, y);
}
Graphics::FloatPoint QuickTimeDecoder::getPanAngles(int16 x, int16 y) {
PanoTrackHandler *track = (PanoTrackHandler *)getTrack(_panoTrack->targetTrack);
PanoSampleDesc *desc = (PanoSampleDesc *)_panoTrack->sampleDescs[0];
Common::Point pos = track->projectPoint(x, y);
Graphics::FloatPoint res;
res.x = desc->_hPanStart + (desc->_hPanStart - desc->_hPanEnd) * (float)pos.x / (float)desc->_sceneSizeY;
res.y = desc->_vPanTop + (desc->_vPanBottom - desc->_vPanTop) * (float)pos.y / (float)desc->_sceneSizeX;
return res;
}
void QuickTimeDecoder::lookupHotspot(int16 x, int16 y) {
PanoTrackHandler *track = (PanoTrackHandler *)getTrack(_panoTrack->targetTrack);
Common::Point hotspotPoint = track->projectPoint(x, y);
if (hotspotPoint.x < 0) {
_rolloverHotspot = nullptr;
_rolloverHotspotID = 0;
} else {
int hotspotId = (int)(((PanoTrackHandler *)getTrack(_panoTrack->targetTrack))->_constructedHotspots->getPixel(hotspotPoint.y, hotspotPoint.x));
_rolloverHotspotID = hotspotId;
if (hotspotId && _currentSample != -1) {
if (!_rolloverHotspot || _rolloverHotspot->id != hotspotId)
_rolloverHotspot = _panoTrack->panoSamples[_currentSample].hotSpotTable.get(hotspotId);
if (!_rolloverHotspot)
debugC(1, kDebugLevelGVideo, "Hotspot id: %d has no data", hotspotId);
else
debugC(1, kDebugLevelGVideo, "Hotspot id: %d is of type '%s'", hotspotId, tag2str(_rolloverHotspot->type));
} else {
_rolloverHotspot = nullptr;
}
}
}
Common::String QuickTimeDecoder::getHotSpotName(int id) {
if (id <= 0)
return "";
PanoHotSpot *hotspot = _panoTrack->panoSamples[_currentSample].hotSpotTable.get(id);
return _panoTrack->panoSamples[_currentSample].strTable.getString(hotspot->nameStrOffset);
}
const QuickTimeDecoder::PanoHotSpot *QuickTimeDecoder::getHotSpotByID(int id) {
return _panoTrack->panoSamples[_currentSample].hotSpotTable.get(id);
}
const QuickTimeDecoder::PanoNavigation *QuickTimeDecoder::getHotSpotNavByID(int id) {
const QuickTimeDecoder::PanoHotSpot *hotspot = getHotSpotByID(id);
if (hotspot->type != MKTAG('n','a','v','g'))
return nullptr;
return _panoTrack->panoSamples[_currentSample].navTable.get(hotspot->typeData);
}
void QuickTimeDecoder::setClickedHotSpot(int id) {
_clickedHotspot = getHotSpotByID(id);
_clickedHotspotID = id;
}
///////////////////////////////
// INTERACTIVITY
//////////////////////////////
void QuickTimeDecoder::handleQuit() {
if (_repeatTimerActive) {
_repeatTimerActive = false;
g_system->getTimerManager()->removeTimerProc(&repeatCallback);
}
}
void QuickTimeDecoder::handleMouseMove(int16 x, int16 y) {
if (_qtvrType == QTVRType::OBJECT)
handleObjectMouseMove(x, y);
else if (_qtvrType == QTVRType::PANORAMA)
handlePanoMouseMove(x, y);
updateQTVRCursor(x, y);
}
void QuickTimeDecoder::handleObjectMouseMove(int16 x, int16 y) {
if (!_isMouseButtonDown)
return;
VideoTrackHandler *track = (VideoTrackHandler *)_nextVideoTrack;
// HACK: FIXME: Hard coded for now
const int sensitivity = 10;
const float speedFactor = 0.1f;
int16 mouseDeltaX = x - _prevMouse.x;
int16 mouseDeltaY = y - _prevMouse.y;
float speedX = (float)mouseDeltaX * speedFactor;
float speedY = (float)mouseDeltaY * speedFactor;
bool changed = false;
if (ABS(mouseDeltaY) >= sensitivity) {
int newFrame = track->getCurFrame() - round(speedY) * _nav.columns;
if (newFrame >= 0 && newFrame < track->getFrameCount()) {
track->setCurFrame(newFrame);
changed = true;
}
}
if (ABS(mouseDeltaX) >= sensitivity) {
int currentRow = track->getCurFrame() / _nav.columns;
int currentRowStart = currentRow * _nav.columns;
int newFrame = (track->getCurFrame() - (int)roundf(speedX) - currentRowStart) % _nav.columns + currentRowStart;
if (newFrame >= 0 && newFrame < track->getFrameCount()) {
track->setCurFrame(newFrame);
changed = true;
}
}
if (changed) {
_prevMouse.x = x;
_prevMouse.y = y;
}
}
void QuickTimeDecoder::handlePanoMouseMove(int16 x, int16 y) {
_prevMouse.x = x;
_prevMouse.y = y;
lookupHotspot(x, y);
}
#define REPEAT_DELAY 30000
static void repeatCallback(void *data) {
QuickTimeDecoder *decoder = (QuickTimeDecoder *)data;
if (decoder->_isKeyDown)
decoder->handleKey(decoder->_lastKey, true, true);
if (decoder->_isMouseButtonDown)
decoder->handleMouseButton(true, decoder->_prevMouse.x, decoder->_prevMouse.y, true);
}
void QuickTimeDecoder::handleMouseButton(bool isDown, int16 x, int16 y, bool repeat) {
if (_qtvrType == QTVRType::OBJECT)
handleObjectMouseButton(isDown, x, y, repeat);
else if (_qtvrType == QTVRType::PANORAMA)
handlePanoMouseButton(isDown, x, y, repeat);
if (isDown) {
if (!_repeatTimerActive)
g_system->getTimerManager()->installTimerProc(&repeatCallback, REPEAT_DELAY, this, "Mouse Repeat Handler");
_repeatTimerActive = true;
} else {
if (_repeatTimerActive) {
_repeatTimerActive = false;
g_system->getTimerManager()->removeTimerProc(&repeatCallback);
}
}
updateQTVRCursor(x, y);
}
void QuickTimeDecoder::handleObjectMouseButton(bool isDown, int16 x, int16 y, bool repeat) {
if (isDown) {
if (y < _curBbox.top) {
setCurrentRow(getCurrentRow() + 1);
} else if (y > _curBbox.bottom) {
setCurrentRow(getCurrentRow() - 1);
} else if (x < _curBbox.left) {
setCurrentColumn((getCurrentColumn() + 1) % _nav.columns);
} else if (x > _curBbox.right) {
setCurrentColumn((getCurrentColumn() - 1 + _nav.columns) % _nav.columns);
} else {
_prevMouse.x = x;
_prevMouse.y = y;
}
}
_isMouseButtonDown = isDown;
PanoTrackHandler *track = (PanoTrackHandler *)getTrack(_panoTrack->targetTrack);
track->setDirty();
}
void QuickTimeDecoder::handlePanoMouseButton(bool isDown, int16 x, int16 y, bool repeat) {
_isMouseButtonDown = isDown;
lookupHotspot(x, y);
if (isDown && !repeat) {
_prevMouse.x = x;
_prevMouse.y = y;
_mouseDrag.x = x;
_mouseDrag.y = y;
_clickedHotspot = _rolloverHotspot;
_clickedHotspotID = _rolloverHotspotID;
}
if (!repeat && !isDown && _rolloverHotspot && _prevMouse == _mouseDrag) {
if (_rolloverHotspot->type == MKTAG('l','i','n','k')) {
PanoLink *link = _panoTrack->panoSamples[_currentSample].linkTable.get(_rolloverHotspot->typeData);
if (link) {
goToNode(link->toNodeID);
setPanAngle(link->toHPan);
setTiltAngle(link->toVPan);
setFOV(link->toZoom);
}
} else if (_rolloverHotspot->type == MKTAG('n','a','v','g')) {
warning("navg hotpot id #%d not processed", _rolloverHotspot->id);
}
}
// This line is necessary to let the decodeNextFrame() function know that the _isMouseButtonDown
// variable has been updated, otherwise, it won't call projectPanorama() function
if (!isDown) {
PanoTrackHandler *track = (PanoTrackHandler *)getTrack(_panoTrack->targetTrack);
track->setDirty();
}
// Further we have simulated mouse button which are generated by timer, e.g. those
// are used for dragging
if (!repeat)
return;
// HACK: FIXME: Hard coded for now
const int sensitivity = 5;
const float speedFactor = 0.1f;
int16 mouseDeltaX = x - _mouseDrag.x;
int16 mouseDeltaY = y - _mouseDrag.y;
float speedX = (float)mouseDeltaX * speedFactor;
float speedY = (float)mouseDeltaY * speedFactor;
if (ABS(mouseDeltaX) >= sensitivity)
setPanAngle(getPanAngle() - speedX);
if (ABS(mouseDeltaY) >= sensitivity)
setTiltAngle(getTiltAngle() - speedY);
}
void QuickTimeDecoder::handleKey(Common::KeyState &state, bool down, bool repeat) {
if (_qtvrType == QTVRType::OBJECT) {
handleObjectKey(state, down, repeat);
} else if (_qtvrType == QTVRType::PANORAMA) {
handlePanoKey(state, down, repeat);
}
if (down) {
_lastKey = state;
_isKeyDown = true;
if (!_repeatTimerActive)
g_system->getTimerManager()->installTimerProc(&repeatCallback, REPEAT_DELAY, this, "Keyboard Repeat Handler");
_repeatTimerActive = true;
} else {
_isKeyDown = false;
if (_repeatTimerActive) {
_repeatTimerActive = false;
g_system->getTimerManager()->removeTimerProc(&repeatCallback);
}
}
updateQTVRCursor(_prevMouse.x, _prevMouse.y);
}
void QuickTimeDecoder::handleObjectKey(Common::KeyState &state, bool down, bool repeat) {
if (!down)
return;
switch (state.keycode) {
case Common::KEYCODE_LEFT:
nudge("left");
break;
case Common::KEYCODE_RIGHT:
nudge("right");
break;
case Common::KEYCODE_UP:
nudge("up");
break;
case Common::KEYCODE_DOWN:
nudge("bottom");
break;
default:
break;
}
}
void QuickTimeDecoder::handlePanoKey(Common::KeyState &state, bool down, bool repeat) {
PanoTrackHandler *track = (PanoTrackHandler *)getTrack(_panoTrack->targetTrack);
track->setDirty();
if ((state.flags & Common::KBD_SHIFT) && (state.flags & Common::KBD_CTRL)) {
_zoomState = kZoomQuestion;
} else if (state.flags & Common::KBD_SHIFT) {
_zoomState = kZoomIn;
if (!setFOV(getFOV() - 2.0))
_zoomState = kZoomLimit;
} else if (state.flags & Common::KBD_CTRL) {
_zoomState = kZoomOut;
if (!setFOV(getFOV() + 2.0))
_zoomState = kZoomLimit;
} else {
_zoomState = kZoomNone;
}
if (state.keycode == Common::KEYCODE_h && down && !repeat)
renderHotspots(!_renderHotspots);
}
enum {
kCurHand = 129,
kCurGrab = 130,
kCurObjUp = 131,
kCurObjDown = 132,
kCurObjLeft90 = 133,
kCurObjRight90 = 134,
kCurObjLeftM90 = 149,
kCurObjRightM90 = 150,
kCurObjUpLimit = 151,
kCurObjDownLimit = 152,
kCurDirAll = 211,
kCurDirL = 212,
kCurDirR = 213,
kCurDirD = 214,
kCurDirDL = 215,
kCurDirDR = 216,
kCurDirU = 217,
kCurDirUL = 218,
kCurDirUR = 219,
kCurDirAllDown = 220,
kCurDirURDown = 228,
kCursorPano = 480,
kCursorPanoLinkOver = 481,
kCursorPanoLinkDown = 482,
kCursorPanoLinkUp = 482,
kCursorPanoObjOver = 484,
kCursorPanoObjDown = 485,
kCursorPanoObjUp = 486,
kCursorZoomIn = 500,
kCursorZoomOut = 501,
kCursorZoomQuestion = 502,
kCursorZoomLimit = 503,
kCursorPanoNav = 510,
kCursorPanoL = 511,
kCursorPanoLS = 512,
kCursorPanoR = 513,
kCursorPanoRS = 514,
kCursorPanoD = 515,
kCursorPanoDS = 516,
kCursorPanoU = 525,
kCursorPanoUS = 526,
kCursorPano2 = 540,
kCurLastCursor
};
static const char *keyMatrix[] = {
// 76543210
"l.......", // 511
"lL......", // 512
"..r.....", // 513
"..rR....", // 514
"....d...", // 515
"....dD..", // 516
"l...d...", // 517
"l...dD..", // 518
"lL..d...", // 519
"lL..dD..", // 520
"..r.d...", // 521
"..r.dD..", // 522
"..rRd...", // 523
"..rRdD..", // 524
"......u.", // 525
"......uU", // 526
"l.....u.", // 527
"l.....uU", // 528
"lL....u.", // 529
"lL....uU", // 530
"..r...u.", // 531
"..r...uU", // 532
"..rR..u.", // 533
"..rR..uU", // 534
0
};
void QuickTimeDecoder::updateQTVRCursor(int16 x, int16 y) {
if (_qtvrType == QTVRType::OBJECT) {
int tiltIdx = int((-_tiltAngle + 90.0) / 21) * 2;
if (y < _curBbox.top)
setCursor(tiltIdx == 16 ? kCurObjUpLimit : kCurObjUp);
else if (y > _curBbox.bottom)
setCursor(tiltIdx == 0 ? kCurObjDownLimit : kCurObjDown);
else if (x < _curBbox.left)
setCursor(kCurObjLeft90 + tiltIdx);
else if (x > _curBbox.right)
setCursor(kCurObjRight90 + tiltIdx);
else
setCursor(_isMouseButtonDown ? kCurGrab : kCurHand);
} else if (_qtvrType == QTVRType::PANORAMA) {
if (_zoomState != kZoomNone) {
switch (_zoomState) {
case kZoomIn:
setCursor(kCursorZoomIn);
break;
case kZoomOut:
setCursor(kCursorZoomOut);
break;
case kZoomQuestion:
setCursor(kCursorZoomQuestion);
break;
case kZoomLimit:
setCursor(kCursorZoomLimit);
break;
}
return;
}
// Get hotspot cursors
uint32 hsType = MKTAG('u','n','d','f');
if (_rolloverHotspot)
hsType = _rolloverHotspot->type;
int hsOver, hsDown, hsUp;
switch (hsType) {
case MKTAG('l','i','n','k'):
hsOver = kCursorPanoLinkOver;
hsDown = kCursorPanoLinkDown;
hsUp = kCursorPanoLinkUp;
break;
case MKTAG('n','a','v','g'):
debug(3, "Hotspot type: %s", tag2str((uint32)hsType));
// TODO FIXME: Implement
// fallthrough
default:
hsOver = kCursorPanoObjOver;
hsDown = kCursorPanoObjDown;
hsUp = kCursorPanoObjUp;
break;
}
if (_rolloverHotspot) {
if (_rolloverHotspot->mouseOverCursorID)
hsOver = _rolloverHotspot->mouseOverCursorID;
if (_rolloverHotspot->mouseDownCursorID)
hsDown = _rolloverHotspot->mouseDownCursorID;
if (_rolloverHotspot->mouseUpCursorID)
hsUp = _rolloverHotspot->mouseUpCursorID;
}
int sensitivity = 5;
if (!_isMouseButtonDown) {
setCursor(_rolloverHotspot ? hsOver : kCursorPano);
} else {
int res = 0;
PanoSampleDesc *desc = (PanoSampleDesc *)_panoTrack->sampleDescs[0];
bool pano360 = !(desc->_hPanStart != desc->_hPanStart && (desc->_hPanStart != 0.0 || desc->_hPanStart != 360.0));
// left
if (x < _mouseDrag.x - sensitivity) {
res |= 1;
res <<= 1;
// left stop
if (!pano360 && _panAngle <= desc->_hPanStart + _hfov / 2)
res |= 1;
res <<= 1;
} else {
res <<= 2;
}
// right
if (x > _mouseDrag.x + sensitivity) {
res |= 1;
res <<= 1;
// right stop
if (!pano360 && _panAngle >= desc->_hPanEnd - _hfov / 2)
res |= 1;
res <<= 1;
} else {
res <<= 2;
}
// down
if (y > _mouseDrag.y + sensitivity) {
res |= 1;
res <<= 1;
// down stop
if (_tiltAngle <= desc->_vPanBottom + _fov / 2)
res |= 1;
res <<= 1;
} else {
res <<= 2;
}
// up
if (y < _mouseDrag.y - sensitivity) {
res |= 1;
res <<= 1;
// up stop
if (_tiltAngle >= desc->_vPanTop - _fov / 2)
res |= 1;
} else {
res <<= 1;
}
(void)hsUp;
setCursor(_cursorDirMap[res] ? _cursorDirMap[res] : _rolloverHotspot ? hsDown : kCursorPanoNav);
}
}
}
void QuickTimeDecoder::cleanupCursors() {
if (!_cursorCache)
return;
for (int i = 0; i < kCurLastCursor; i++)
delete _cursorCache[i];
free(_cursorCache);
_cursorCache = nullptr;
}
void QuickTimeDecoder::setCursor(int curId) {
_currentQTVRCursor = curId;
if (!_dataBundle) {
_dataBundle = Common::makeZipArchive(MACGUI_DATA_BUNDLE);
if (!_dataBundle) {
warning("QTVR: Couldn't load data bundle '%s'.", MACGUI_DATA_BUNDLE);
}
}
if (!_cursorCache) {
_cursorCache = (Graphics::Cursor **)calloc(kCurLastCursor, sizeof(Graphics::Cursor *));
computeInteractivityZones();
memset(_cursorDirMap, 0, 256 * sizeof(int));
int n = 511;
for (const char **p = keyMatrix; *p; p++, n++) {
int res = 0;
for (int i = 0; i < 8; i++) {
res <<= 1;
if ((*p)[i] != '.')
res |= 1;
}
_cursorDirMap[res] = n;
}
}
if (curId >= kCurLastCursor)
error("QTVR: Incorrect cursor ID: %d > %d", curId, kCurLastCursor);
if (!_cursorCache[curId]) {
Common::Path path(Common::String::format("qtvr/CURSOR%d_1.cur", curId));
Common::SeekableReadStream *stream = _dataBundle->createReadStreamForMember(path);
if (!stream) {
warning("QTVR: Cannot load cursor ID %d, file '%s' does not exist", curId, path.toString().c_str());
return;
}
Image::IcoCurDecoder decoder;
if (!decoder.open(*stream, DisposeAfterUse::YES)) {
warning("QTVR: Cannot load cursor ID %d, file '%s' bad format", curId, path.toString().c_str());
return;
}
_cursorCache[curId] = decoder.loadItemAsCursor(0);
}
CursorMan.replaceCursor(_cursorCache[curId]);
CursorMan.showMouse(true);
}
void QuickTimeDecoder::computeInteractivityZones() {
_curBbox.left = MIN(20, getWidth() / 10);
_curBbox.right = getWidth() - _curBbox.left;
_curBbox.top = MIN(20, getHeight() / 10);
_curBbox.bottom = getHeight() - _curBbox.top;
}
} // End of namespace Video