output: allow aspect-ratio specific images

This commit is contained in:
Marcin Kurczewski 2025-04-21 21:49:29 +02:00
parent fbc26fcba6
commit daf6dc3020
10 changed files with 250 additions and 32 deletions

View file

@ -690,7 +690,18 @@ default game flow for examples.
</td> </td>
<td><code>path</code></td> <td><code>path</code></td>
<td>String</td> <td>String</td>
<td> Displays the specified picture for a fixed time. </td> <td>
Displays the specified picture for a fixed time.
Files that are needed to function only with a specific aspect ratio can
be placed in a directory adjacent to the main image, named according to
the aspect ratio for example, 4x3/title.png or 16x10/title.png. The
game won't attempt to match these precisely; instead, it will select the
file with the aspect ratio closest to the game's viewport. The main image
designated by <code>path</code> is presumed to have a 16:9 aspect ratio
for this purpose, and as such there's no need for 16x9-specific
directory.<br/>
This logic applies to all images.
</td>
</tr> </tr>
<tr valign="top"> <tr valign="top">
<td><code>display_time</code></td> <td><code>display_time</code></td>

View file

@ -3,6 +3,7 @@
- added an ability to customize the water color [see the reference](/docs/GAME_FLOW.md#water-color-table) (#1532) - added an ability to customize the water color [see the reference](/docs/GAME_FLOW.md#water-color-table) (#1532)
- added support for a hex water color notation (eg. `#80FFFF`) in the game flow file - added support for a hex water color notation (eg. `#80FFFF`) in the game flow file
- added support for antitriggers, like TR2+ (#2580) - added support for antitriggers, like TR2+ (#2580)
- added support for aspect ratio-specific images (#1840)
- changed the `draw_distance_min` and `draw_distance_max` to `fog_start` and `fog_end` - changed the `draw_distance_min` and `draw_distance_max` to `fog_start` and `fog_end`
- changed `Select Detail` dialog title to `Graphic Options` - changed `Select Detail` dialog title to `Graphic Options`
- changed the number of static mesh slots from 50 to 256 (#2734) - changed the number of static mesh slots from 50 to 256 (#2734)

View file

@ -13,6 +13,7 @@
- added the ability for spike walls to be reset (antitriggered) - added the ability for spike walls to be reset (antitriggered)
- added the current music track and timestamp to the savegame so they now persist on load (#2579) - added the current music track and timestamp to the savegame so they now persist on load (#2579)
- added waterfalls to the savegame so that they now persist on load (#2686) - added waterfalls to the savegame so that they now persist on load (#2686)
- added support for aspect ratio-specific images (#1840)
- changed savegame files to be stored in the `saves` directory (#2087) - changed savegame files to be stored in the `saves` directory (#2087)
- changed the default fog distance to 22 tiles cutting off at 30 tiles to match TR1X (#1622) - changed the default fog distance to 22 tiles cutting off at 30 tiles to match TR1X (#1622)
- changed the number of static mesh slots from 50 to 256 (#2734) - changed the number of static mesh slots from 50 to 256 (#2734)

View file

@ -0,0 +1,209 @@
#include "game/output/background.h"
#include "debug.h"
#include "filesystem.h"
#include "game/viewport.h"
#include "log.h"
#include "memory.h"
#include "strings.h"
#include "utils.h"
#include "vector.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define M_RELATIVE_ERROR(a, b) ABS((a) - (b)) / (b)
typedef struct {
char *file_name;
float diff;
} M_CANDIDATE;
static char *m_LastPath = nullptr;
static IMAGE *M_CreateImageFromPath(const char *path);
static float M_GetScreenAspectRatio(void);
static int M_CompareCandidates(const void *a, const void *b);
static VECTOR *M_ScanCandidates(const char *path, size_t *dir_len_out);
static bool M_TryLoadCandidates(
const char *path, VECTOR *candidates, size_t dir_len);
static void M_FreeCandidates(VECTOR *candidates);
static IMAGE *M_CreateImageFromPath(const char *const path)
{
if (TR_VERSION == 1) {
return Image_CreateFromFileInto(
path, Viewport_GetWidth(), Viewport_GetHeight(), IMAGE_FIT_SMART);
} else {
return Image_CreateFromFile(path);
}
}
static float M_GetScreenAspectRatio(void)
{
return Viewport_GetWidth() / (float)Viewport_GetHeight();
}
static int M_CompareCandidates(const void *const a, const void *const b)
{
const M_CANDIDATE *const c1 = a;
const M_CANDIDATE *const c2 = b;
if (c1->diff < c2->diff) {
return -1;
}
if (c1->diff > c2->diff) {
return 1;
}
return 0;
}
static VECTOR *M_ScanCandidates(
const char *const path, size_t *const dir_len_out)
{
VECTOR *candidates = nullptr;
const char *last_slash = strrchr(path, '/');
const char *last_backslash = strrchr(path, '\\');
const char *last_sep =
last_slash > last_backslash ? last_slash : last_backslash;
size_t dir_len;
char *dir_path;
if (last_sep != nullptr) {
dir_len = last_sep - path;
dir_path = String_Format("%.*s", (int)dir_len, path);
} else {
dir_len = 0;
dir_path = Memory_DupStr(".");
}
const char *file_name = last_sep ? last_sep + 1 : path;
const char *ext_ptr = strrchr(file_name, '.');
const float screen_ratio = M_GetScreenAspectRatio();
void *const dir_handle = File_OpenDirectory(dir_path);
if (dir_handle == nullptr) {
goto finish;
}
candidates = Vector_Create(sizeof(M_CANDIDATE));
const char *entry;
while ((entry = File_ReadDirectory(dir_handle)) != nullptr) {
// Match the file itself, and assume it's of 16:9 aspect ratio.
if (String_Equivalent(entry, file_name)) {
const float ratio = 16.0f / 9.0f;
Vector_Add(
candidates,
&(M_CANDIDATE) {
.file_name = Memory_DupStr(file_name),
.diff = M_RELATIVE_ERROR(ratio, screen_ratio),
});
}
// Match directories with pattern: <width>x<height>
int32_t w = 0, h = 0;
if (sscanf(entry, "%dx%d", &w, &h) == 2) {
const float ratio = w / (float)h;
Vector_Add(
candidates,
&(M_CANDIDATE) {
.file_name = String_Format("%s/%s", entry, file_name),
.diff = M_RELATIVE_ERROR(ratio, screen_ratio),
});
}
}
File_CloseDirectory(dir_handle);
finish:
*dir_len_out = dir_len;
Memory_FreePointer(&dir_path);
return candidates;
}
static bool M_TryLoadCandidates(
const char *const path, VECTOR *const candidates, const size_t dir_len)
{
for (int32_t i = 0; i < candidates->count; i++) {
const M_CANDIDATE *candidate = Vector_Get(candidates, i);
char *full_path;
if (dir_len > 0) {
full_path = String_Format(
"%.*s/%s", (int32_t)dir_len, path, candidate->file_name);
} else {
full_path = String_Format("%s", candidate->file_name);
}
IMAGE *const image = M_CreateImageFromPath(full_path);
Memory_FreePointer(&full_path);
if (image != nullptr) {
Output_LoadBackgroundFromImage(image);
Image_Free(image);
return true;
}
}
return false;
}
static void M_FreeCandidates(VECTOR *const candidates)
{
if (candidates == nullptr) {
return;
}
for (int32_t i = 0; i < candidates->count; i++) {
M_CANDIDATE *const candidate = Vector_Get(candidates, i);
Memory_Free(candidate->file_name);
}
Vector_Free(candidates);
}
bool Output_LoadBackgroundFromFile(const char *const path)
{
LOG_INFO("Loading image %s", path);
size_t dir_len = 0;
bool result = false;
// Try aspect-ratio specific directories.
VECTOR *candidates = M_ScanCandidates(path, &dir_len);
if (candidates != nullptr) {
M_CANDIDATE *raw_candidates = Vector_GetData(candidates);
qsort(
raw_candidates, candidates->count, sizeof(M_CANDIDATE),
M_CompareCandidates);
for (int32_t i = 0; i < candidates->count; i++) {
const M_CANDIDATE *const candidate = Vector_Get(candidates, i);
LOG_INFO(
"Found candidate %s (diff=%.02f)", candidate->file_name,
candidate->diff);
}
if (M_TryLoadCandidates(path, candidates, dir_len)) {
result = true;
}
}
if (!result) {
// Fallback to the main image.
IMAGE *const image = M_CreateImageFromPath(path);
if (image != nullptr) {
result = true;
Output_LoadBackgroundFromImage(image);
Image_Free(image);
}
}
M_FreeCandidates(candidates);
if (result) {
char *prev = m_LastPath;
m_LastPath = Memory_DupStr(path);
Memory_FreePointer(&prev);
}
return result;
}
char *Output_GetLastBackgroundPath(void)
{
return m_LastPath;
}
void Output_ClearLastBackgroundPath(void)
{
Memory_FreePointer(&m_LastPath);
}

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "./output/background.h"
#include "./output/common.h" #include "./output/common.h"
#include "./output/const.h" #include "./output/const.h"
#include "./output/draw.h" #include "./output/draw.h"

View file

@ -0,0 +1,15 @@
#pragma once
#include "../../engine/image.h"
bool Output_LoadBackgroundFromFile(const char *path);
extern bool Output_LoadBackgroundFromImage(const IMAGE *image);
extern void Output_LoadBackgroundFromObject(void);
extern void Output_UnloadBackground(void);
extern void Output_DrawBackground(void);
// TODO: make these functions private once output module is consolidated
char *Output_GetLastBackgroundPath(void);
void Output_ClearLastBackgroundPath(void);

View file

@ -10,12 +10,7 @@ extern bool Output_MakeScreenshot(const char *path);
extern void Output_BeginScene(void); extern void Output_BeginScene(void);
extern void Output_EndScene(void); extern void Output_EndScene(void);
extern bool Output_LoadBackgroundFromFile(const char *file_name);
extern void Output_LoadBackgroundFromObject(void);
extern void Output_UnloadBackground(void);
extern void Output_DrawBlackRectangle(int32_t opacity); extern void Output_DrawBlackRectangle(int32_t opacity);
extern void Output_DrawBackground(void);
extern void Output_DrawPolyList(void); extern void Output_DrawPolyList(void);
extern void Output_SetupBelowWater(bool is_underwater); extern void Output_SetupBelowWater(bool is_underwater);

View file

@ -181,6 +181,7 @@ sources = [
'game/objects/names.c', 'game/objects/names.c',
'game/objects/traps/movable_block.c', 'game/objects/traps/movable_block.c',
'game/objects/vars.c', 'game/objects/vars.c',
'game/output/background.c',
'game/output/common.c', 'game/output/common.c',
'game/output/textures.c', 'game/output/textures.c',
'game/packer.c', 'game/packer.c',

View file

@ -63,7 +63,6 @@ static int32_t m_SurfaceHeight = 0;
static GFX_2D_SURFACE *m_PictureSurface = nullptr; static GFX_2D_SURFACE *m_PictureSurface = nullptr;
static GFX_2D_SURFACE *m_TextureSurfaces[GFX_MAX_TEXTURES] = { nullptr }; static GFX_2D_SURFACE *m_TextureSurfaces[GFX_MAX_TEXTURES] = { nullptr };
static char *m_BackdropImagePath = nullptr;
static const char *m_ImageExtensions[] = { static const char *m_ImageExtensions[] = {
".png", ".jpg", ".jpeg", ".pcx", nullptr, ".png", ".jpg", ".jpeg", ".pcx", nullptr,
}; };
@ -318,7 +317,6 @@ static void M_DrawLightningSegment(const LIGHTNING *const lightning)
vertices[vtx_idx].b = color.b; \ vertices[vtx_idx].b = color.b; \
vertices[vtx_idx].a = 128.0f; vertices[vtx_idx].a = 128.0f;
// clang-format off // clang-format off
LOG_INFO("%d %d %d, %d %d %d, %d", p0.x, p0.y, p0.z, p1.x, p1.y, p1.z, t1);
SET(0, p0.x, p0.y, p0.z, blue); SET(0, p0.x, p0.y, p0.z, blue);
SET(1, p0.x + t1 / 2, p0.y, p0.z, white); SET(1, p0.x + t1 / 2, p0.y, p0.z, white);
SET(2, p1.x + t2 / 2, p1.y, p1.z, white); SET(2, p1.x + t2 / 2, p1.y, p1.z, white);
@ -424,7 +422,7 @@ void Output_Shutdown(void)
m_Renderer3D = nullptr; m_Renderer3D = nullptr;
} }
GFX_Context_Detach(); GFX_Context_Detach();
Memory_FreePointer(&m_BackdropImagePath); Output_ClearLastBackgroundPath();
} }
void Output_SetWindowSize(int32_t width, int32_t height) void Output_SetWindowSize(int32_t width, int32_t height)
@ -467,8 +465,9 @@ void Output_ApplyRenderSettings(void)
GFX_3D_Renderer_SetAnisotropyFilter( GFX_3D_Renderer_SetAnisotropyFilter(
m_Renderer3D, g_Config.rendering.anisotropy_filter); m_Renderer3D, g_Config.rendering.anisotropy_filter);
if (m_BackdropImagePath != nullptr) { const char *const last_path = Output_GetLastBackgroundPath();
Output_LoadBackgroundFromFile(m_BackdropImagePath); if (last_path != nullptr) {
Output_LoadBackgroundFromFile(last_path);
} }
} }
@ -781,21 +780,9 @@ void Output_DrawUISprite(
} }
} }
bool Output_LoadBackgroundFromFile(const char *const path) bool Output_LoadBackgroundFromImage(const IMAGE *const image)
{ {
ASSERT(path != nullptr); M_DownloadBackdropSurface(image);
const char *old_path = m_BackdropImagePath;
m_BackdropImagePath = File_GuessExtension(path, m_ImageExtensions);
Memory_FreePointer(&old_path);
IMAGE *const img = Image_CreateFromFileInto(
m_BackdropImagePath, Viewport_GetWidth(), Viewport_GetHeight(),
IMAGE_FIT_SMART);
if (img == nullptr) {
return false;
}
M_DownloadBackdropSurface(img);
Image_Free(img);
return true; return true;
} }
@ -808,7 +795,7 @@ void Output_LoadBackgroundFromObject(void)
void Output_UnloadBackground(void) void Output_UnloadBackground(void)
{ {
M_DownloadBackdropSurface(nullptr); M_DownloadBackdropSurface(nullptr);
Memory_FreePointer(&m_BackdropImagePath); Output_ClearLastBackgroundPath();
} }
void Output_DrawLightningSegment( void Output_DrawLightningSegment(

View file

@ -18,6 +18,7 @@
#include <libtrx/game/scaler.h> #include <libtrx/game/scaler.h>
#include <libtrx/log.h> #include <libtrx/log.h>
#include <libtrx/memory.h> #include <libtrx/memory.h>
#include <libtrx/strings.h>
#include <libtrx/utils.h> #include <libtrx/utils.h>
typedef enum { typedef enum {
@ -738,14 +739,9 @@ BACKGROUND_TYPE Output_GetBackgroundType(void)
return m_BackgroundType; return m_BackgroundType;
} }
bool Output_LoadBackgroundFromFile(const char *const file_name) bool Output_LoadBackgroundFromImage(const IMAGE *const image)
{ {
IMAGE *const image = Image_CreateFromFile(file_name);
if (image == nullptr) {
return false;
}
Render_LoadBackgroundFromImage(image); Render_LoadBackgroundFromImage(image);
Image_Free(image);
m_BackgroundType = BK_IMAGE; m_BackgroundType = BK_IMAGE;
return true; return true;
} }
@ -774,6 +770,7 @@ void Output_UnloadBackground(void)
{ {
Render_UnloadBackground(); Render_UnloadBackground();
m_BackgroundType = BK_TRANSPARENT; m_BackgroundType = BK_TRANSPARENT;
Output_ClearLastBackgroundPath();
} }
void Output_InsertBackPolygon( void Output_InsertBackPolygon(