Switched positional audio to collision coordinate system.

Refactored positional audio.
This commit is contained in:
Muzychenko Andrey 2022-06-01 16:19:27 +03:00
parent c93e11ee6b
commit 8017734de4
19 changed files with 214 additions and 143 deletions

View File

@ -15,6 +15,7 @@
#include "TBall.h"
#include "render.h"
#include "options.h"
#include "Sound.h"
gdrv_bitmap8* DebugOverlay::dbScreen = nullptr;
@ -109,10 +110,14 @@ void DebugOverlay::DrawOverlay()
if (options::Options.DebugOverlayAllEdges)
DrawAllEdges();
// Draw ball collision
// Draw ball collision info
if (options::Options.DebugOverlayBallPosition || options::Options.DebugOverlayBallEdges)
DrawBallInfo();
// Draw positions associated with currently playing sound channels
if (options::Options.DebugOverlaySounds)
DrawSoundPositions();
// Restore render target
SDL_SetRenderTarget(winmain::Renderer, initialRenderTarget);
SDL_SetRenderDrawColor(winmain::Renderer,
@ -133,18 +138,18 @@ void DebugOverlay::DrawBoxGrid()
SDL_SetRenderDrawColor(winmain::Renderer, 0, 255, 0, 255);
for (int x = 0; x <= edgeMan.MaxBoxX; x++)
{
vector2 boxPt{ x * edgeMan.AdvanceX + edgeMan.X , edgeMan.Y };
vector2 boxPt{ x * edgeMan.AdvanceX + edgeMan.MinX , edgeMan.MinY };
auto pt1 = proj::xform_to_2d(boxPt);
boxPt.Y = edgeMan.MaxBoxY * edgeMan.AdvanceY + edgeMan.Y;
boxPt.Y = edgeMan.MaxBoxY * edgeMan.AdvanceY + edgeMan.MinY;
auto pt2 = proj::xform_to_2d(boxPt);
SDL_RenderDrawLine(winmain::Renderer, pt1.X, pt1.Y, pt2.X, pt2.Y);
}
for (int y = 0; y <= edgeMan.MaxBoxY; y++)
{
vector2 boxPt{ edgeMan.X, y * edgeMan.AdvanceY + edgeMan.Y };
vector2 boxPt{ edgeMan.MinX, y * edgeMan.AdvanceY + edgeMan.MinY };
auto pt1 = proj::xform_to_2d(boxPt);
boxPt.X = edgeMan.MaxBoxX * edgeMan.AdvanceX + edgeMan.X;
boxPt.X = edgeMan.MaxBoxX * edgeMan.AdvanceX + edgeMan.MinX;
auto pt2 = proj::xform_to_2d(boxPt);
SDL_RenderDrawLine(winmain::Renderer, pt1.X, pt1.Y, pt2.X, pt2.Y);
@ -225,6 +230,19 @@ void DebugOverlay::DrawAllSprites()
}
}
void DebugOverlay::DrawSoundPositions()
{
auto& edgeMan = *TTableLayer::edge_manager;
SDL_SetRenderDrawColor(winmain::Renderer, 200, 0, 200, 255);
for (auto& posNorm : Sound::Channels)
{
auto pos3D = edgeMan.DeNormalizeBox(posNorm.Position);
auto pos2D = proj::xform_to_2d(pos3D);
SDL_RenderDrawCircle(winmain::Renderer, pos2D.X, pos2D.Y, 7);
}
}
void DebugOverlay::DrawCicleType(circle_type& circle)
{
vector2 linePt{ circle.Center.X + sqrt(circle.RadiusSq), circle.Center.Y };

View File

@ -20,4 +20,5 @@ private:
static void DrawAllEdges();
static void DrawBallInfo();
static void DrawAllSprites();
static void DrawSoundPositions();
};

View File

@ -5,7 +5,7 @@
int Sound::num_channels;
bool Sound::enabled_flag = false;
int* Sound::TimeStamps = nullptr;
std::vector<ChannelInfo> Sound::Channels{};
int Sound::Volume = MIX_MAX_VOLUME;
bool Sound::Init(int channels, bool enableFlag, int volume)
@ -37,106 +37,79 @@ void Sound::Deactivate()
void Sound::Close()
{
delete[] TimeStamps;
TimeStamps = nullptr;
Channels.clear();
Mix_CloseAudio();
Mix_Quit();
}
void Sound::PlaySound(Mix_Chunk* wavePtr, int time, TPinballComponent *soundSource, const char* info)
void Sound::PlaySound(Mix_Chunk* wavePtr, int time, TPinballComponent* soundSource, const char* info)
{
if (wavePtr && enabled_flag)
{
if (Mix_Playing(-1) == num_channels)
{
auto oldestChannel = std::min_element(TimeStamps, TimeStamps + num_channels) - TimeStamps;
auto cmp = [](const ChannelInfo& a, const ChannelInfo& b)
{
return a.TimeStamp < b.TimeStamp;
};
auto min = std::min_element(Channels.begin(), Channels.end(), cmp);
auto oldestChannel = std::distance(Channels.begin(), min);
Mix_HaltChannel(oldestChannel);
}
auto channel = Mix_PlayChannel(-1, wavePtr, 0);
if (channel != -1) {
TimeStamps[channel] = time;
if (options::Options.SoundStereo) {
/* Think 3D sound positioning, where:
* - x goes from 0 to 1, left to right on the screen,
* - y goes from 0 to 1, top to bottom on the screen,
* - z goes from 0 to infinity, from table-level to the sky.
*
* We position the listener at the bottom center of the table,
* at 0.5 height, so roughly a table half-length. Coords of
* the listener are thus {0.5, 1.0, 0.5}.
*
* We use basic trigonometry to calculate the angle and distance
* from a sound source to the listener.
*
* Mix_SetPosition expects an angle in (Sint16)degrees, where
* 0 degrees is in front, 90 degrees is to the right, and so on.
* Mix_SetPosition expects a (Uint8)distance from 0 (near) to 255 (far).
*/
if (channel != -1)
{
Channels[channel].TimeStamp = time;
if (options::Options.SoundStereo)
{
// Positional audio uses collision grid 2D coordinates normalized to [0, 1]
// Point (0, 0) is bottom left table corner; point (1, 1) is top right table corner.
// Z is defined as: 0 at table level, positive axis goes up from table surface.
/* Get the sound source position. */
vector2 coordinates;
/* Some sounds are unpositioned; for that case the caller sends
* a NULL pointer as a soundSource; in those cases we position
* the sound at the center top of the table.
*/
if (!soundSource) {
coordinates.X = 0.5f;
coordinates.Y = 0.0f;
// Get the source sound position.
// Sound without position are assumed to be at the center top of the table.
vector3 soundPos{};
if (soundSource)
{
auto soundPos2D = soundSource->get_coordinates();
soundPos = {soundPos2D.X, soundPos2D.Y, 0.0f};
}
else {
coordinates = soundSource->get_coordinates();
};
/* Player position. */
auto pX = 0.5f;
auto pY = 1.0f;
auto pZ = 0.5f;
/* Calculate lengths of three sides of a triangle.
* ptos (Player-to-sound): distance from listener to the sound source,
* ptom (player-to-middle): distance from listener to the sound source
* when the latter is repositioned to the
* X center,
* stom (sound-to-middle): distance from ptos to ptom.
*/
auto ptos = sqrt(((coordinates.X - pX) * (coordinates.X - pX)) + ((coordinates.Y - pY) * (coordinates.Y - pY)) + (pZ * pZ));
auto ptom = sqrt(((coordinates.Y - pY) * (coordinates.Y - pY)) + (pZ * pZ));
auto stom = fabs(coordinates.X - 0.5);
/* Calculate the angle using the law of cosines and acos().
* That will return an angle in radians, e.g. in the [0,PI] range;
* we remap to [0,180], and cast to an integer.
*/
Sint16 angle = (Sint16)(acos(((stom * stom) - (ptos * ptos) - (ptom * ptom)) / (-2.0f * ptos * ptom)) * 180.0f / IM_PI);
/* Because we are using distances to calculate the angle,
* we now have no clue if the sound is to the right or the
* left. If the sound is to the right, the current value
* is good, but to the left, we need substract it from 360.
*/
if (coordinates.X < 0.5) {
angle = (360 - angle);
else
{
soundPos = {0.5f, 1.0f, 0.0f};
}
Channels[channel].Position = soundPos;
/* Distance from listener to the ball (ptos) is roughly
* in the [0.5,1.55] range; remap to 50-155 by multiplying
* by 100 and cast to an integer. */
Uint8 distance = (Uint8)(100.0f * ptos);
Mix_SetPosition(channel, angle, distance);
// Listener is positioned at the bottom center of the table,
// at 0.5 height, so roughly a table half - length.
vector3 playerPos = {0.5f, 0.0f, 0.5f};
auto soundDir = maths::vector_sub(soundPos, playerPos);
/* Output position of each sound emitted so we can verify
* the sanity of the implementation.
*/
/*
printf("X: %3.3f Y: %3.3f Angle: %3d Distance: %3d, Object: %s\n",
coordinates.X,
coordinates.Y,
angle,
// Find sound angle from positive Y axis in clockwise direction with atan2
// Remap atan2 output from (-Pi, Pi] to [0, 2 * Pi)
auto angle = fmodf(atan2(soundDir.X, soundDir.Y) + Pi * 2, Pi * 2);
auto angleDeg = angle * 180.0f / Pi;
auto angleSdl = static_cast<Sint16>(angleDeg);
// Distance from listener to the sound position is roughly in the [0, ~1.22] range.
// Remap to [0, 122] by multiplying by 100 and cast to an integer.
auto distance = static_cast<Uint8>(100.0f * maths::magnitude(soundDir));
// Mix_SetPosition expects an angle in (Sint16)degrees, where
// angle 0 is due north, and rotates clockwise as the value increases.
// Mix_SetPosition expects a (Uint8)distance from 0 (near) to 255 (far).
Mix_SetPosition(channel, angleSdl, distance);
// Output position of each sound emitted so we can verify
// the sanity of the implementation.
/*printf("X: %3.3f Y: %3.3f Angle: %3.3f Distance: %3d, Object: %s\n",
soundPos.X,
soundPos.Y,
angleDeg,
distance,
info
);
*/
);*/
}
}
}
@ -164,8 +137,7 @@ void Sound::SetChannels(int channels)
channels = 8;
num_channels = channels;
delete[] TimeStamps;
TimeStamps = new int[num_channels]();
Channels.resize(num_channels);
Mix_AllocateChannels(num_channels);
SetVolume(Volume);
}

View File

@ -1,10 +1,18 @@
#pragma once
#include "maths.h"
#include "TPinballComponent.h"
struct ChannelInfo
{
int TimeStamp;
vector2 Position;
};
class Sound
{
public:
static std::vector<ChannelInfo> Channels;
static bool Init(int channels, bool enableFlag, int volume);
static void Enable(bool enableFlag);
static void Activate();
@ -18,6 +26,5 @@ public:
private:
static int num_channels;
static bool enabled_flag;
static int* TimeStamps;
static int Volume;
};

View File

@ -9,6 +9,7 @@
#include "proj.h"
#include "render.h"
#include "TPinballTable.h"
#include "TTableLayer.h"
TBall::TBall(TPinballTable* table) : TPinballComponent(table, -1, false)
{
@ -138,9 +139,5 @@ void TBall::throw_ball(TBall* ball, vector3* direction, float angleMult, float s
vector2 TBall::get_coordinates()
{
vector2 coordinates;
vector2i pos2D = proj::xform_to_2d(Position);
coordinates.X = (float)pos2D.X / PinballTable->Width;
coordinates.Y = (float)pos2D.Y / PinballTable->Height;
return coordinates;
return TTableLayer::edge_manager->NormalizeBox(Position);
}

View File

@ -8,16 +8,18 @@
#include "TEdgeSegment.h"
#include "TTableLayer.h"
TEdgeManager::TEdgeManager(float posX, float posY, float width, float height)
TEdgeManager::TEdgeManager(float xMin, float yMin, float width, float height)
{
X = posX;
Y = posY;
Width = width;
Height = height;
MinX = xMin;
MinY = yMin;
MaxX = MinX + width;
MaxY = MinY + height;
MaxBoxX = 10;
MaxBoxY = 15;
AdvanceX = width / static_cast<float>(MaxBoxX);
AdvanceY = height / static_cast<float>(MaxBoxY);
AdvanceXInv = 1.0f / AdvanceX;
AdvanceYInv = 1.0f / AdvanceY;
BoxArray = new TEdgeBox[MaxBoxX * MaxBoxY];
}
@ -28,12 +30,12 @@ TEdgeManager::~TEdgeManager()
int TEdgeManager::box_x(float x)
{
return std::max(0, std::min(static_cast<int>(floor((x - X) * AdvanceXInv)), MaxBoxX - 1));
return std::max(0, std::min(static_cast<int>(floor((x - MinX) / AdvanceX)), MaxBoxX - 1));
}
int TEdgeManager::box_y(float y)
{
return std::max(0, std::min(static_cast<int>(floor((y - Y) * AdvanceYInv)), MaxBoxY - 1));
return std::max(0, std::min(static_cast<int>(floor((y - MinY) / AdvanceY)), MaxBoxY - 1));
}
int TEdgeManager::increment_box_x(int x)
@ -180,8 +182,8 @@ float TEdgeManager::FindCollisionDistance(ray_type* ray, TBall* ball, TEdgeSegme
for (auto indexX = xBox0, indexY = yBox0; indexX != xBox1 || indexY != yBox1;)
{
// Calculate y from indexY and from line formula
auto yDiscrete = (indexY + yBias) * AdvanceY + Y;
auto ylinear = ((indexX + xBias) * AdvanceX + X) * dyDx + precomp;
auto yDiscrete = (indexY + yBias) * AdvanceY + MinY;
auto ylinear = ((indexX + xBias) * AdvanceX + MinX) * dyDx + precomp;
if (dirY == 1 ? ylinear >= yDiscrete : ylinear <= yDiscrete)
{
// Advance indexY when discrete value is ahead/behind
@ -207,3 +209,23 @@ float TEdgeManager::FindCollisionDistance(ray_type* ray, TBall* ball, TEdgeSegme
return distance;
}
vector2 TEdgeManager::NormalizeBox(vector2 pt) const
{
// Standard PB Box ranges: X [-8, 8]; Y [-14, 15]; Top right corner: (-8, -14)
// Bring them to: X [0, 16]; Y [0, 29]; Top right corner: (0, 0)
auto x = Clamp(pt.X, MinX, MaxX) + abs(MinX);
auto y = Clamp(pt.Y, MinY, MaxY) + abs(MinY);
// Normalize and invert to: X [0, 1]; Y [0, 1]; Top right corner: (1, 1)
x /= Width; y /= Height;
return vector2{ 1 - x, 1 - y };
}
vector2 TEdgeManager::DeNormalizeBox(vector2 pt) const
{
// Undo normalization by applying steps in reverse
auto x = (1 - pt.X) * Width - abs(MinX);
auto y = (1 - pt.Y) * Height - abs(MinY);
return vector2{ x, y };
}

View File

@ -14,7 +14,7 @@ struct field_effect_type
class TEdgeManager
{
public:
TEdgeManager(float posX, float posY, float width, float height);
TEdgeManager(float xMin, float yMin, float width, float height);
~TEdgeManager();
void FieldEffects(TBall* ball, struct vector2* dstVec);
int box_x(float x);
@ -25,15 +25,19 @@ public:
void add_field_to_box(int x, int y, field_effect_type* field);
int TestGridBox(int x, int y, float* distPtr, TEdgeSegment** edgeDst, ray_type* ray, TBall* ball, int edgeIndex);
float FindCollisionDistance(ray_type* ray, TBall* ball, TEdgeSegment** edge);
vector2 NormalizeBox(vector2 pt) const;
vector2 DeNormalizeBox(vector2 pt) const;
float AdvanceX;
float AdvanceY;
float AdvanceXInv;
float AdvanceYInv;
int MaxBoxX;
int MaxBoxY;
float X;
float Y;
float MinX;
float MinY;
float MaxX;
float MaxY;
float Width;
float Height;
TEdgeBox* BoxArray;
TEdgeSegment* EdgeArray[1000]{};
};

View File

@ -102,8 +102,8 @@ void TLine::place_in_grid()
for (auto indexX = xBox0, indexY = yBox0; indexX != xBox1 || indexY != yBox1;)
{
// Calculate y from indexY and from line formula
auto yDiscrete = (indexY + yBias) * edgeMan->AdvanceY + edgeMan->Y;
auto ylinear = ((indexX + xBias) * edgeMan->AdvanceX + edgeMan->X) * dyDx + precomp;
auto yDiscrete = (indexY + yBias) * edgeMan->AdvanceY + edgeMan->MinY;
auto ylinear = ((indexX + xBias) * edgeMan->AdvanceX + edgeMan->MinX) * dyDx + precomp;
if (dirY == 1 ? ylinear >= yDiscrete : ylinear <= yDiscrete)
{
// Advance indexY when discrete value is ahead/behind

View File

@ -1,8 +1,10 @@
#include "pch.h"
#include "TPinballComponent.h"
#include "loader.h"
#include "proj.h"
#include "render.h"
#include "TPinballTable.h"
#include "TTableLayer.h"
TPinballComponent::TPinballComponent(TPinballTable* table, int groupIndex, bool loadVisuals)
{
@ -72,9 +74,13 @@ TPinballComponent::TPinballComponent(TPinballTable* table, int groupIndex, bool
rootBmp->YPosition - table->YOffset,
&bmp1Rect);
// Sound position = center of root visual, reverse-projected, normalized.
auto& rect = RenderSprite->BmpRect;
VisualPosNormX = (rect.XPosition + (rect.Width / 2.0f)) / PinballTable->Width;
VisualPosNormY = (rect.YPosition + (rect.Height / 2.0f)) / PinballTable->Height;
vector2i pos2D{ rect.XPosition + rect.Width / 2, rect.YPosition + rect.Height / 2 };
auto pos3D = proj::ReverseXForm(pos2D);
auto posNorm = TTableLayer::edge_manager->NormalizeBox(pos3D);
VisualPosNormX = posNorm.X;
VisualPosNormY = posNorm.Y;
}
}
GroupIndex = groupIndex;

View File

@ -71,37 +71,33 @@ TTableLayer::TTableLayer(TPinballTable* table): TCollisionComponent(table, -1, f
Threshold = visual.Kicker.Threshold;
Boost = 15.0f;
auto visArrPtr = visual.FloatArr;
Unknown1F = std::min(visArrPtr[0], std::min(visArrPtr[2], visArrPtr[4]));
Unknown2F = std::min(visArrPtr[1], std::min(visArrPtr[3], visArrPtr[5]));
Unknown3F = std::max(visArrPtr[0], std::max(visArrPtr[2], visArrPtr[4]));
Unknown4F = std::max(visArrPtr[1], std::max(visArrPtr[3], visArrPtr[5]));
auto a2 = Unknown4F - Unknown2F;
auto a1 = Unknown3F - Unknown1F;
edge_manager = new TEdgeManager(Unknown1F, Unknown2F, a1, a2);
auto edgePoints = reinterpret_cast<vector2*>(visual.FloatArr);
XMin = std::min(edgePoints[0].X, std::min(edgePoints[1].X, edgePoints[2].X));
YMin = std::min(edgePoints[0].Y, std::min(edgePoints[1].Y, edgePoints[2].Y));
XMax = std::max(edgePoints[0].X, std::max(edgePoints[1].X, edgePoints[2].X));
YMax = std::max(edgePoints[0].Y, std::max(edgePoints[1].Y, edgePoints[2].Y));
for (auto visFloatArrCount = visual.FloatArrCount; visFloatArrCount > 0; visFloatArrCount--)
auto height = YMax - YMin;
auto width = XMax - XMin;
edge_manager = new TEdgeManager(XMin, YMin, width, height);
for (auto i = 0; i < visual.FloatArrCount; i++)
{
auto line = new TLine(this,
&ActiveFlag,
visual.CollisionGroup,
visArrPtr[2],
visArrPtr[3],
visArrPtr[0],
visArrPtr[1]);
if (line)
{
edgePoints[i + 1].X,
edgePoints[i + 1].Y,
edgePoints[i].X,
edgePoints[i].Y);
line->place_in_grid();
EdgeList.push_back(line);
}
visArrPtr += 2;
}
Field.CollisionGroup = -1;
Field.ActiveFlag = &ActiveFlag;
Field.CollisionComp = this;
edges_insert_square(Unknown2F, Unknown1F, Unknown4F, Unknown3F, nullptr,
edges_insert_square(YMin, XMin, YMax, XMax, nullptr,
&Field);
}
@ -134,10 +130,10 @@ void TTableLayer::edges_insert_square(float y0, float x0, float y1, float x1, TE
int xMaxBox = edge_manager->box_x(xMax);
int yMaxBox = edge_manager->box_y(yMax);
float boxX = static_cast<float>(xMinBox) * edge_manager->AdvanceX + edge_manager->X;
float boxX = static_cast<float>(xMinBox) * edge_manager->AdvanceX + edge_manager->MinX;
for (int indexX = xMinBox; indexX <= xMaxBox; ++indexX)
{
float boxY = static_cast<float>(yMinBox) * edge_manager->AdvanceY + edge_manager->Y;
float boxY = static_cast<float>(yMinBox) * edge_manager->AdvanceY + edge_manager->MinY;
for (int indexY = yMinBox; indexY <= yMaxBox; ++indexY)
{
if (xMax >= boxX && xMin <= boxX + edge_manager->AdvanceX &&
@ -182,10 +178,10 @@ void TTableLayer::edges_insert_circle(circle_type* circle, TEdgeSegment* edge, f
xMaxBox = edge_manager->increment_box_x(xMaxBox);
yMaxBox = edge_manager->increment_box_y(yMaxBox);
vec1.X = static_cast<float>(dirX) * edge_manager->AdvanceX + edge_manager->X;
vec1.X = static_cast<float>(dirX) * edge_manager->AdvanceX + edge_manager->MinX;
for (auto indexX = dirX; indexX <= xMaxBox; ++indexX)
{
vec1.Y = static_cast<float>(dirY) * edge_manager->AdvanceY + edge_manager->Y;
vec1.Y = static_cast<float>(dirY) * edge_manager->AdvanceY + edge_manager->MinY;
for (int indexY = dirY; indexY <= yMaxBox; ++indexY)
{
auto vec1XAdv = vec1.X + edge_manager->AdvanceX;

View File

@ -21,10 +21,10 @@ public:
static void edges_insert_circle(circle_type* circle, TEdgeSegment* edge, field_effect_type* field);
gdrv_bitmap8* VisBmp;
float Unknown1F;
float Unknown2F;
float Unknown3F;
float Unknown4F;
float XMin;
float YMin;
float XMax;
float YMax;
float GraityDirX;
float GraityDirY;
float GraityMult;

View File

@ -220,6 +220,11 @@ vector2 maths::vector_sub(const vector2& vec1, const vector2& vec2)
return { vec1.X - vec2.X, vec1.Y - vec2.Y };
}
vector3 maths::vector_sub(const vector3& vec1, const vector3& vec2)
{
return { vec1.X - vec2.X, vec1.Y - vec2.Y, vec1.Z - vec2.Z };
}
vector2 maths::vector_mul(const vector2& vec1, float val)
{
return { vec1.X * val, vec1.Y * val };

View File

@ -108,6 +108,7 @@ public:
static float magnitude(const vector3& vec);
static void vector_add(vector2& vec1Dst, const vector2& vec2);
static vector2 vector_sub(const vector2& vec1, const vector2& vec2);
static vector3 vector_sub(const vector3& vec1, const vector3& vec2);
static vector2 vector_mul(const vector2& vec1, float val);
static float basic_collision(TBall* ball, vector2* nextPosition, vector2* direction, float elasticity,
float smoothness,

View File

@ -112,6 +112,7 @@ void options::InitPrimary()
Options.DebugOverlayBallEdges = get_int("Debug Overlay Ball Edges", true);
Options.DebugOverlayCollisionMask = get_int("Debug Overlay Collision Mask", true);
Options.DebugOverlaySprites = get_int("Debug Overlay Sprites", true);
Options.DebugOverlaySounds = get_int("Debug Overlay Sounds", true);
}
void options::InitSecondary()
@ -159,6 +160,7 @@ void options::uninit()
set_int("Debug Overlay Ball Edges", Options.DebugOverlayBallEdges);
set_int("Debug Overlay Collision Mask", Options.DebugOverlayCollisionMask);
set_int("Debug Overlay Sprites", Options.DebugOverlaySprites);
get_int("Debug Overlay Sounds", Options.DebugOverlaySounds);
}

View File

@ -89,6 +89,7 @@ struct optionsStruct
bool DebugOverlayBallEdges;
bool DebugOverlayCollisionMask;
bool DebugOverlaySprites;
bool DebugOverlaySounds;
};
struct ControlRef

View File

@ -111,4 +111,6 @@ constexpr const char* PlatformDataPaths[2] =
#endif
};
constexpr float Pi = 3.14159265358979323846f;
#endif //PCH_H

View File

@ -48,6 +48,40 @@ vector2i proj::xform_to_2d(const vector2& vec)
return xform_to_2d(vec3);
}
vector3 proj::ReverseXForm(const vector2i& vec)
{
// Pinball perspective projection matrix, the same for all tables resolutions:
// X: 1.000000 Y: 0.000000 Z: 0.000000 W: 0.000000
// X: 0.000000 Y: -0.913545 Z: 0.406737 W: 3.791398
// X: 0.000000 Y: -0.406737 Z: -0.913545 W: 24.675402
// X: 0.000000 Y: 0.000000 Z: 0.000000 W: 1.000000
// Let A = -0.913545, B = 0.406737, F = 3.791398, G = 24.675402
// Then forward projection can be expressed as:
// x1 = x0
// y1 = y0 * A + z0 * B + F
// z1 = -y0 * B + z0 * A + G
// x2 = x1 / z1 = x0 / z1
// y2 = y1 / z1
// z2 = z1 / z1 = 1
// Reverse projection: find x0, y0, z0 given x2 and y2
// y0 from y2 and z0, based on substitution in y2 = y1 / z1
// y0 = (y2 * (A * z0 + G) - B * z0 - F)/(A + B * y2)
// x0 from x2, y0 and z0, based on substitution in x2 = x0 / z1
// x0 = (x2 * (A * z0 - B * y0 + G)
// This pair of equations is solvable with fixed z0; most commonly z0 = 0
// PB projection also includes scaling and offset, this can be undone before the main calculations
// x2 = x0 * D / z1 + cX
// x0 = ((x2 - cX) / D) * z1
const auto A = matrix.Row1.Y, B = matrix.Row1.Z, F = matrix.Row1.W, G = matrix.Row2.W, D = d_;
const auto x2 = (vec.X - centerx) /D, y2 = (vec.Y - centery)/D, z0 = 0.0f;
auto y0 = (y2 * (A * z0 + G) - B * z0 - F) / (A + B * y2);
auto x0 = x2 * (A * z0 - B * y0 + G);
return {x0, y0, z0};
}
vector2i proj::xform_to_2d(const vector3& vec)
{
float projCoef;

View File

@ -26,6 +26,7 @@ public:
static float z_distance(const vector3& vec);
static vector2i xform_to_2d(const vector3& vec);
static vector2i xform_to_2d(const vector2& vec);
static vector3 ReverseXForm(const vector2i& vec);
static void recenter(float centerX, float centerY);
private:
static mat4_row_major matrix;

View File

@ -608,6 +608,8 @@ void winmain::RenderUi()
Options.DebugOverlayBallPosition ^= true;
if (ImGui::MenuItem("Ball Box Edges", nullptr, Options.DebugOverlayBallEdges))
Options.DebugOverlayBallEdges ^= true;
if (ImGui::MenuItem("Sound Positions", nullptr, Options.DebugOverlaySounds))
Options.DebugOverlaySounds ^= true;
if (ImGui::MenuItem("Apply Collision Mask", nullptr, Options.DebugOverlayCollisionMask))
Options.DebugOverlayCollisionMask ^= true;
ImGui::EndMenu();