Files
scummvm-cursorfix/video/qtvr_decoder.cpp
2026-02-02 04:50:13 +01:00

2037 lines
63 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";
////////////////////////////////////////////
// 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 = 2.0;
if (entry->_maximumZoom == 0.0)
entry->_maximumZoom = abs(entry->_vPanTop - entry->_vPanBottom);
debugC(2, kDebugLevelGVideo, " version: %d.%d sceneTrackID: %d loResSceneTrackID: %d hotSpotTrackID: %d",
entry->_majorVersion, entry->_minorVersion, entry->_sceneTrackID, entry->_loResSceneTrackID, entry->_hotSpotTrackID);
debugC(2, kDebugLevelGVideo, " hpan: [%f - %f] vpan: [%f - %f] zoom: [%f - %f]",
entry->_hPanStart, entry->_hPanEnd, entry->_vPanTop, entry->_vPanBottom, entry->_minimumZoom, entry->_maximumZoom);
debugC(2, kDebugLevelGVideo, " sceneDims: [%d x %d] frames: %d sceneFrm: [%d x %d] bpp: %d",
entry->_sceneSizeX, entry->_sceneSizeY, entry->_numFrames, entry->_sceneNumFramesX,
entry->_sceneNumFramesY, entry->_sceneColorDepth);
debugC(2, kDebugLevelGVideo, " hotspotDims: [%d x %d] hotspotFrm: [%d x %d] bpp: %d",
entry->_hotSpotSizeX, entry->_hotSpotSizeY, entry->_hotSpotNumFramesX, entry->_hotSpotNumFramesY,
entry->_hotSpotColorDepth);
return entry;
}
void QuickTimeDecoder::closeQTVR() {
delete _dataBundle;
_dataBundle = nullptr;
cleanupCursors();
}
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) {
// Curretnly the warp mode 1 is not implemented correctly
// So, forcing the warp mode 1 to warp mode 2
if (warpMode == 1) warpMode = 2;
_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];
float panRange = abs(desc->_hPanEnd - desc->_hPanStart);
angle = fmod(angle, panRange);
if (desc->_hPanStart != desc->_hPanEnd && (desc->_hPanStart != 0.0 || desc->_hPanEnd != 360.0)) {
if (angle < desc->_hPanStart + _hfov) {
angle = desc->_hPanStart + _hfov;
} else if (angle > desc->_hPanEnd - _hfov) {
angle = desc->_hPanEnd - _hfov;
}
}
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 == 0.0f && _zoomState == kZoomNone) // This is reference to default FOV
fov = _panoTrack->panoInfo.defZoom;
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);
debugC(3, kDebugLevelGVideo, "QuickTimeDecoder::setFOV: fov: %f (was %f)", fov, _fov);
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 (numNodes: %d)", nodeID, _panoTrack->panoSamples.size());
idx = 0;
}
_currentSample = idx;
debugC(3, kDebugLevelGVideo, "QuickTimeDecoder::goToNode(): Moving to nodeID: %d (index: %d)", nodeID, 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;
if (_constructedHotspots) {
_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->_tiltAngle, _decoder->_panAngle);
}
const Graphics::Surface *QuickTimeDecoder::PanoTrackHandler::decodeNextFrame() {
if (!_isPanoConstructed)
return nullptr;
// inject fake key/mouse events if button is held down
if (_decoder->_isKeyDown)
_decoder->handleKey(_decoder->_lastKey, true, true);
if (_decoder->_isMouseButtonDown)
_decoder->handleMouseButton(true, _decoder->_prevMouse.x, _decoder->_prevMouse.y, true);
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();
}
if (_decoder->_cursorDirty) {
_decoder->_cursorDirty = false;
_decoder->updateQTVRCursor(_decoder->_cursorPos.x, _decoder->_cursorPos.y);
}
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() {
if (_decoder->_panoTrack->panoInfo.nodes.size() == 0) {
// This is a single node panorama
// We add one node to the list, so the rest of our code
// is happy
PanoTrackSample *sample = &_parent->panoSamples[0];
_decoder->_panoTrack->panoInfo.nodes.resize(1);
_decoder->_panoTrack->panoInfo.nodes[0].nodeID = sample->hdr.nodeID;
_decoder->_panoTrack->panoInfo.nodes[0].timestamp = 0;
_decoder->_panoTrack->panoInfo.defNodeID = sample->hdr.nodeID;
}
_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;
if (_constructedHotspots) {
_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 panoInfo", 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");
// _upscaleLevel = 0 means _contructedPano has just been constructed, hasn't been upscaled yet
// or that the upscaledConstructedPanorama has upscaled a different panorama, not the current constructedPano
_upscaleLevel = 0;
if (desc->_hotSpotTrackID) {
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 || !_constructedHotspots)
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 panRange = abs(desc->_hPanEnd - desc->_hPanStart);
float angleT = fmod((panRange - panAngle) / panRange, 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) {
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);
if (!track->_constructedHotspots)
return;
Common::Point hotspotPoint = track->projectPoint(x, y);
if (hotspotPoint.x < 0) {
_rolloverHotspot = nullptr;
_rolloverHotspotID = 0;
} else {
int hotspotId = (int)track->_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() {
}
void QuickTimeDecoder::handleMouseMove(int16 x, int16 y) {
if (_qtvrType == QTVRType::OBJECT)
handleObjectMouseMove(x, y);
else if (_qtvrType == QTVRType::PANORAMA)
handlePanoMouseMove(x, y);
_cursorDirty = true;
_cursorPos = Common::Point(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);
}
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);
_cursorDirty = true;
_cursorPos = Common::Point(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;
}
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;
} else {
_isKeyDown = false;
}
_cursorDirty = true;
_cursorPos = Common::Point(_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) {
if (track->_constructedHotspots)
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->_hPanEnd && (desc->_hPanStart != 0.0 || desc->_hPanEnd != 360.0));
debugC(4, kDebugLevelGVideo, "pano360: %d _panAngle: %f [%f - %f] +%f -%f fov: %f hfov: %f", pano360, _panAngle, desc->_hPanStart, desc->_hPanEnd, desc->_hPanStart + _fov, desc->_hPanEnd - _fov, _fov, _hfov);
// left
if (x < _mouseDrag.x - sensitivity) {
res |= 1;
res <<= 1;
// left stop
if (!pano360 && _panAngle >= desc->_hPanEnd - _hfov)
res |= 1;
res <<= 1;
} else {
res <<= 2;
}
// right
if (x > _mouseDrag.x + sensitivity) {
res |= 1;
res <<= 1;
// right stop
if (!pano360 && _panAngle <= desc->_hPanStart + _hfov)
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