diff --git a/.gitattributes b/.gitattributes index 837a0ac25a8..083dcf3ec3d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,7 @@ *.tbl text eol=lf *.tbm text eol=lf *.sdr text eol=lf + +# Test inputs that are deliberately not UTF-8, kept out of text diffs +test/test_data/graphics/texture_material/data/maps/mat_sharp_s.mat binary +test/test_data/graphics/texture_material/data/maps/mat_truncated.mat binary diff --git a/code/bmpman/bm_internal.h b/code/bmpman/bm_internal.h index b5a0aa82605..7a511b8165c 100644 --- a/code/bmpman/bm_internal.h +++ b/code/bmpman/bm_internal.h @@ -11,6 +11,8 @@ #include "bmpman/bmpman.h" +struct texture_material; + #include /** @@ -63,6 +65,8 @@ struct bitmap_entry { int dir_type; //!< which directory this was loaded from (to skip other locations with same name) + const texture_material* material; //!< settings from the texture's .mat file, or nullptr + // compressed bitmap stuff (.dds) - RT please take a look at this and tell me if we really need it size_t mem_taken; //!< How much memory does this bitmap use? - UnknownPlayer int num_mipmaps; //!< number of mipmap levels, we need to read all of them diff --git a/code/bmpman/bmpman.cpp b/code/bmpman/bmpman.cpp index 9b9788adb67..2f6c1a1d4bf 100644 --- a/code/bmpman/bmpman.cpp +++ b/code/bmpman/bmpman.cpp @@ -22,6 +22,7 @@ #include "globalincs/systemvars.h" #include "graphics/2d.h" #include "graphics/matrix.h" +#include "graphics/texture_material.h" #include "io/key.h" #include "io/timer.h" #include "jpgutils/jpgutils.h" @@ -1112,6 +1113,13 @@ int bm_is_valid(int handle) { return entry->handle == handle; } +const texture_material* bm_get_texture_material(int handle) { + if (!bm_is_valid(handle)) + return nullptr; + + return bm_get_entry(handle)->material; +} + // Load an image and validate it while retrieving information for later use // Input: type = current BM_TYPE_* @@ -1272,12 +1280,17 @@ int bm_load(const char *real_filename, int dir_type) { return -1; } + const texture_material* material = nullptr; + // Lets find out what type it is { // see if it's already loaded (checks for any type with filename) if (bm_load_sub_fast(filename, &handle, dir_type)) return handle; + // before any file is opened or slot filled, since a bad .mat can throw in unit tests + material = texture_material_find(filename, dir_type); + // if we are still here then we need to fall back to a file-based search int rval = bm_load_sub_slow(filename, BM_NUM_TYPES, bm_ext_list, &img_cfp, dir_type); @@ -1337,6 +1350,7 @@ int bm_load(const char *real_filename, int dir_type) { entry->mem_taken = (size_t)bm_size; entry->dir_type = dir_type; entry->handle = handle; + entry->material = material; entry->load_count++; @@ -1600,6 +1614,8 @@ int bm_load_animation(const char *real_filename, int *nframes, int *fps, int *ke // used later if EFF type strcpy_s(clean_name, filename); + const texture_material* material = nullptr; + // Lets find out what type it is { int handle = -1; @@ -1623,6 +1639,8 @@ int bm_load_animation(const char *real_filename, int *nframes, int *fps, int *ke return handle; } + material = texture_material_find(clean_name, dir_type); + // if we are still here then we need to fall back to a file-based search int rval = bm_load_sub_slow(filename, BM_ANI_NUM_TYPES, bm_ani_ext_list, &img_cfp, dir_type); @@ -1832,6 +1850,7 @@ int bm_load_animation(const char *real_filename, int *nframes, int *fps, int *ke entry->num_mipmaps = mm_lvl; entry->mem_taken = (size_t)img_size; entry->dir_type = dir_type; + entry->material = material; entry->load_count++; diff --git a/code/bmpman/bmpman.h b/code/bmpman/bmpman.h index d52a01f5125..46ef78f1601 100644 --- a/code/bmpman/bmpman.h +++ b/code/bmpman/bmpman.h @@ -389,6 +389,15 @@ void bm_unlock(int handle); */ int bm_is_valid(int handle); +struct texture_material; + +/** + * @brief Gets the settings from the bitmap's .mat file + * + * @returns The settings, or nullptr if the handle is invalid or the bitmap has no .mat file + */ +const texture_material* bm_get_texture_material(int handle); + /** * @brief Gets info on the bitmap indexed by handle. * diff --git a/code/cfile/cfile.cpp b/code/cfile/cfile.cpp index 12f6df421a9..e010fff4353 100644 --- a/code/cfile/cfile.cpp +++ b/code/cfile/cfile.cpp @@ -34,6 +34,7 @@ #include "cfilesystem.h" +#include #include char Cfile_root_dir[CFILE_ROOT_DIRECTORY_LEN] = ""; @@ -52,7 +53,7 @@ cf_pathtype Pathtypes[CF_MAX_PATH_TYPES] = { // Root must be index 1!! { CF_TYPE_ROOT, "", ".mve .ogg", CF_TYPE_ROOT }, { CF_TYPE_DATA, "data", ".cfg .txt", CF_TYPE_ROOT }, - { CF_TYPE_MAPS, "data" DIR_SEPARATOR_STR "maps", ".pcx .ani .eff .tga .jpg .png .dds .ktx", CF_TYPE_DATA }, + { CF_TYPE_MAPS, "data" DIR_SEPARATOR_STR "maps", ".pcx .ani .eff .tga .jpg .png .dds .ktx .mat", CF_TYPE_DATA }, { CF_TYPE_TEXT, "data" DIR_SEPARATOR_STR "text", ".txt .net", CF_TYPE_DATA }, { CF_TYPE_MODELS, "data" DIR_SEPARATOR_STR "models", ".pof", CF_TYPE_DATA }, { CF_TYPE_TABLES, "data" DIR_SEPARATOR_STR "tables", ".tbl .tbm .lua", CF_TYPE_DATA }, @@ -68,7 +69,7 @@ cf_pathtype Pathtypes[CF_MAX_PATH_TYPES] = { { CF_TYPE_MOVIES, "data" DIR_SEPARATOR_STR "movies", ".mve .msb .ogg .mp4 .srt .webm .png",CF_TYPE_DATA }, { CF_TYPE_INTERFACE, "data" DIR_SEPARATOR_STR "interface", ".pcx .ani .dds .tga .eff .png .jpg .rml .rcss .ktx", CF_TYPE_DATA }, { CF_TYPE_FONT, "data" DIR_SEPARATOR_STR "fonts", ".vf .ttf .otf", CF_TYPE_DATA }, - { CF_TYPE_EFFECTS, "data" DIR_SEPARATOR_STR "effects", ".ani .eff .pcx .neb .tga .jpg .png .dds .ktx .sdr .vert .frag .glsl", CF_TYPE_DATA }, + { CF_TYPE_EFFECTS, "data" DIR_SEPARATOR_STR "effects", ".ani .eff .pcx .neb .tga .jpg .png .dds .ktx .sdr .vert .frag .glsl .mat", CF_TYPE_DATA }, { CF_TYPE_HUD, "data" DIR_SEPARATOR_STR "hud", ".pcx .ani .eff .tga .jpg .png .dds .ktx", CF_TYPE_DATA }, { CF_TYPE_PLAYERS, "data" DIR_SEPARATOR_STR "players", ".hcf", /* DON'T add pilot files here!! */ CF_TYPE_DATA }, { CF_TYPE_PLAYER_IMAGES, "data" DIR_SEPARATOR_STR "players" DIR_SEPARATOR_STR "images", ".pcx .png .dds .ktx", CF_TYPE_PLAYERS }, @@ -1107,26 +1108,37 @@ void cfread_string(char *buf, int n, CFILE *file) void cfread_string_len(char *buf, int buffer_size, CFILE *file) { - int len; - len = cfread_int(file); - Assertion( (len <= buffer_size), "String cannot fit in buffer, len: %i, buffer: %i", len, buffer_size); - if (len) { - cfread(buf, len, 1, file); - Assertion(len != buffer_size || buf[len - 1] == 0, "Unterminated string does not fit in buffer"); + Assertion(buffer_size > 0, "cfread_string_len needs room for at least the terminator, buffer: %i", buffer_size); + + int len = cfread_int(file); + int to_read = std::clamp(len, 0, buffer_size - 1); + + if (to_read > 0 && cfread(buf, to_read, 1, file) != 1) { + to_read = 0; } + buf[to_read] = 0; - buf[MIN(len, buffer_size - 1)] = 0; + if (len > to_read) { + if (len > buffer_size) { + mprintf(("cfread_string_len: a string of %d characters was cut to fit a buffer of %d in %s\n", len, buffer_size, cf_get_filename(file))); + } + // skip what didn't fit so the next read starts after this string + cfseek(file, len - to_read, CF_SEEK_CUR); + } } SCP_string cfread_string_len(CFILE *file) { int len = cfread_int(file); + // a corrupt length must not make us allocate more than the file can still hold + len = std::clamp(len, 0, std::max(cfilelength(file) - cftell(file), 0)); SCP_string str; str.resize(len); - if (len) - cfread(&str[0], len, 1, file); + if (len > 0 && cfread(str.data(), len, 1, file) != 1) { + str.clear(); + } return str; } diff --git a/code/cfile/cfile.h b/code/cfile/cfile.h index 7c720ac2371..eddebcec64c 100644 --- a/code/cfile/cfile.h +++ b/code/cfile/cfile.h @@ -320,8 +320,9 @@ void cfread_string(char *buf,int n, CFILE *file); /** * @brief Read a fixed length string that is not null-terminated, with the length stored in file. * - * If it is not null-terminated, the length of the string *must* be less than the size of the buffer (to make room for one) - * + * Reads at most buffer_size - 1 characters and skips the rest of the string in the file, so the next read + * starts after it. A negative length reads nothing. + * * @param buf Pre-allocated array to store string * @param buffer_size Size of pre-allocated array * @param file File to read from @@ -332,6 +333,9 @@ void cfread_string_len(char *buf,int buffer_size, CFILE *file); /** * @brief Read a string from the file where the length is stored in the file + * + * A negative length reads nothing, and a length past the end of the file reads only what is left. + * * @param file The file to read the string from * @return The string that was read */ diff --git a/code/def_files/data/effects/decal-f.sdr b/code/def_files/data/effects/decal-f.sdr index 34d65a67ba8..85e68110b8c 100644 --- a/code/def_files/data/effects/decal-f.sdr +++ b/code/def_files/data/effects/decal-f.sdr @@ -73,6 +73,8 @@ uniform decalInfoData { int diffuse_blend_mode; int glow_blend_mode; + float glow_intensity; + float glow_hdr_multiplier; }; vec3 computeViewPosition(vec2 textureCoord) { @@ -180,9 +182,9 @@ void main() { // We have a valid glow map vec4 color = texture(glowMap, vec3(tex_coord, float(glow_index))); - color.rgb = srgb_to_linear(color.rgb) * GLOW_MAP_SRGB_MULTIPLIER; + color.rgb = srgb_to_linear(color.rgb) * glow_hdr_multiplier; - color.rgb *= GLOW_MAP_INTENSITY; + color.rgb *= glow_intensity; if (glow_blend_mode == 0) { // Normal alpha blending diff --git a/code/def_files/data/effects/decal-v.sdr b/code/def_files/data/effects/decal-v.sdr index 42d99146327..d5af5031da9 100644 --- a/code/def_files/data/effects/decal-v.sdr +++ b/code/def_files/data/effects/decal-v.sdr @@ -51,6 +51,8 @@ uniform decalInfoData { int diffuse_blend_mode; int glow_blend_mode; + float glow_intensity; + float glow_hdr_multiplier; }; void main() { diff --git a/code/def_files/data/effects/lighting.sdr b/code/def_files/data/effects/lighting.sdr index 799617b52eb..57944a72a86 100644 --- a/code/def_files/data/effects/lighting.sdr +++ b/code/def_files/data/effects/lighting.sdr @@ -6,6 +6,7 @@ const int LT_CONE = 3; // A cone light, like a flood light const int LT_AMBIENT = 4; // Directionless ambient light const float SPEC_FACTOR_NO_SPEC_MAP = 0.1; +// no longer used here; kept for mods that ship their own shaders referencing them const float GLOW_MAP_INTENSITY = 1.5; const float GLOW_MAP_SRGB_MULTIPLIER = 3.0; const float PI = 3.14159f; diff --git a/code/def_files/data/effects/main-f.sdr b/code/def_files/data/effects/main-f.sdr index 746fe0973b0..06502862099 100644 --- a/code/def_files/data/effects/main-f.sdr +++ b/code/def_files/data/effects/main-f.sdr @@ -89,6 +89,8 @@ uniform modelData { int sMiscmapIndex; float alphaMult; int flags; + float glowIntensity; + float glowHdrMultiplier; }; #ifdef VULKAN @@ -444,9 +446,9 @@ void main() #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_TEAMCOLOR #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_MISC #prereplace IF_FLAG MODEL_SDR_FLAG_HDR - glowColor = srgb_to_linear(glowColor) * GLOW_MAP_SRGB_MULTIPLIER; + glowColor = srgb_to_linear(glowColor) * glowHdrMultiplier; #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_HDR - emissiveColor.rgb += glowColor * GLOW_MAP_INTENSITY; + emissiveColor.rgb += glowColor * glowIntensity; #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_GLOW #prereplace IF_FLAG MODEL_SDR_FLAG_ALPHA_MULT diff --git a/code/def_files/data/effects/main-g.sdr b/code/def_files/data/effects/main-g.sdr index e348c386dfa..9e20de1e1e1 100644 --- a/code/def_files/data/effects/main-g.sdr +++ b/code/def_files/data/effects/main-g.sdr @@ -83,6 +83,8 @@ layout (std140) uniform modelData { float alphaMult; int flags; + float glowIntensity; + float glowHdrMultiplier; }; in VertexOutput { diff --git a/code/def_files/data/effects/main-v.sdr b/code/def_files/data/effects/main-v.sdr index a0adda205a5..458566be598 100644 --- a/code/def_files/data/effects/main-v.sdr +++ b/code/def_files/data/effects/main-v.sdr @@ -103,6 +103,8 @@ uniform modelData { float alphaMult; int flags; + float glowIntensity; + float glowHdrMultiplier; }; #ifdef VULKAN diff --git a/code/graphics/decal_draw_list.cpp b/code/graphics/decal_draw_list.cpp index b0f81848da6..3b546833549 100644 --- a/code/graphics/decal_draw_list.cpp +++ b/code/graphics/decal_draw_list.cpp @@ -5,6 +5,7 @@ #include "render/3d.h" #include "tracing/tracing.h" #include "light.h" +#include "graphics/texture_material.h" namespace { @@ -96,6 +97,11 @@ void decal_draw_list::prepare_global_data() { bm_get_base_frame(batch_info.normal)); info->diffuse_blend_mode = draw_info.first.material.get_blend_mode(0) == ALPHA_BLEND_ADDITIVE ? 1 : 0; info->glow_blend_mode = draw_info.first.material.get_blend_mode(2) == ALPHA_BLEND_ADDITIVE ? 1 : 0; + // glow-only decals have no diffuse texture to name the .mat after + const auto mat = bm_get_texture_material(batch_info.diffuse >= 0 ? batch_info.diffuse : batch_info.glow); + const auto glow = mat != nullptr ? mat->glow : texture_glow_settings(); + info->glow_intensity = texture_glow_intensity(glow); + info->glow_hdr_multiplier = texture_glow_hdr_multiplier(glow); } } diff --git a/code/graphics/material.cpp b/code/graphics/material.cpp index 5a57d0359e6..22c0fefea70 100644 --- a/code/graphics/material.cpp +++ b/code/graphics/material.cpp @@ -737,6 +737,18 @@ void model_material::reset_alpha_mult() { Alpha_mult = 1.0f; } +void model_material::set_glow(const texture_glow_settings& glow) { + Glow = glow; +} + +float model_material::get_glow_intensity() const { + return texture_glow_intensity(Glow); +} + +float model_material::get_glow_hdr_multiplier() const { + return texture_glow_hdr_multiplier(Glow); +} + uint model_material::get_shader_flags() const { uint Shader_flags = 0; diff --git a/code/graphics/material.h b/code/graphics/material.h index 64ad751a62f..1230339385f 100644 --- a/code/graphics/material.h +++ b/code/graphics/material.h @@ -2,6 +2,7 @@ #define _MATERIAL_H #include "graphics/grinternal.h" +#include "graphics/texture_material.h" #include "model/model.h" #include @@ -203,6 +204,8 @@ class model_material : public material bool Use_alpha_mult = false; float Alpha_mult = 1.0f; + texture_glow_settings Glow; + public: model_material(); @@ -263,6 +266,10 @@ class model_material : public material void set_alpha_mult(float alpha); void reset_alpha_mult(); + void set_glow(const texture_glow_settings& glow); + float get_glow_intensity() const; + float get_glow_hdr_multiplier() const; + }; class particle_material : public material diff --git a/code/graphics/texture_material.cpp b/code/graphics/texture_material.cpp new file mode 100644 index 00000000000..039ff5d097d --- /dev/null +++ b/code/graphics/texture_material.cpp @@ -0,0 +1,194 @@ +#include "graphics/texture_material.h" + +#include "cfile/cfile.h" +#include "lighting/lighting_profiles.h" +#include "parse/parselo.h" + +#include + +#include + +namespace { + +// Misses are stored as nullopt so each name is read once; node-based, so bitmap entries can keep pointers into it +SCP_unordered_map, SCP_string_lcase_hash, SCP_string_lcase_equal_to> + Texture_materials; + +constexpr int MAX_MAT_FILE_LEN = 1024 * 1024; + +void mat_warning(const SCP_string& text) +{ + Warning(LOCATION, "%s", text.c_str()); +} + +bool at_line_end() +{ + ignore_gray_space(); + return *Mp == '\0' || *Mp == EOLN || *Mp == CARRIAGE_RETURN || *Mp == ';'; +} + +std::optional parse_glow_value(const char* filename, const char* key, SCP_vector& warnings) +{ + ignore_gray_space(); + const char* start = Mp; + while (*Mp != '\0' && !is_white_space(*Mp) && *Mp != ';') + Mp++; + const SCP_string text(start, Mp - start); + + char* end = nullptr; + const float value = strtof(text.c_str(), &end); + if (text.empty() || *end != '\0' || !std::isfinite(value)) { + warnings.push_back(SCP_string(filename) + ": " + key + " needs a number, not '" + text + "'. Ignoring it."); + return std::nullopt; + } + if (!at_line_end()) + warnings.push_back(SCP_string(filename) + ": extra text after " + key + " " + text + ". Ignoring the extra text."); + + if (value < 0.0f) { + warnings.push_back(SCP_string(filename) + ": " + key + " is negative; using 0."); + return 0.0f; + } + if (value > lighting_profiles::MAX_GLOW_MAP_VALUE) { + warnings.push_back(SCP_string(filename) + ": " + key + " is above " + + std::to_string(static_cast(lighting_profiles::MAX_GLOW_MAP_VALUE)) + "; using that."); + return lighting_profiles::MAX_GLOW_MAP_VALUE; + } + return value; +} + +void parse_texture_material(const char* filename, char* text, texture_material& mat, SCP_vector& warnings) +{ + reset_parse(text); + + int unknown = 0; + // each pass consumes at least one character, so the file length bounds the loop + for (int pass = 0; !check_for_eof(); pass++) { + if (pass > MAX_MAT_FILE_LEN) { + warnings.push_back(SCP_string(filename) + ": stopped reading after " + std::to_string(pass) + " entries."); + break; + } + + if (*Mp == ';') { + advance_to_eoln(nullptr); + continue; + } + + if (optional_string("$Glow Map Intensity:")) { + mat.glow.intensity = parse_glow_value(filename, "$Glow Map Intensity:", warnings); + } else if (optional_string("$Glow Map HDR Multiplier:")) { + mat.glow.hdr_multiplier = parse_glow_value(filename, "$Glow Map HDR Multiplier:", warnings); + } else if (unknown++ == 0) { + warnings.push_back(SCP_string(filename) + ": unrecognized entry [" + next_tokens() + "]. Ignoring it."); + } + advance_to_eoln(nullptr); + } + + if (unknown > 1) + warnings.push_back(SCP_string(filename) + ": " + std::to_string(unknown - 1) + " more unrecognized entries ignored."); +} + +// A NUL (as in UTF-16 without a byte order mark) is valid UTF-8 but would end the parse without a warning +bool is_utf8_text(const SCP_vector& bytes, size_t start, size_t len) +{ + for (size_t i = start; i < len; i++) { + const auto c = static_cast(bytes[i]); + if (c != '\t' && c != '\n' && c != '\r' && (c < 0x20 || c == 0x7f)) + return false; + } + return utf8::find_invalid(bytes.begin() + start, bytes.begin() + len) == bytes.begin() + len; +} + +// Read as-is rather than through the shared table reader, which would decrypt or re-encode the text and could +// outgrow any fixed buffer +bool read_texture_material(const char* filename, int dir_type, texture_material& mat) +{ + CFILE* cfp = cfopen(filename, "rb", dir_type); + if (cfp == nullptr) + return false; + + const int file_len = cfilelength(cfp); + if (file_len <= 0 || file_len > MAX_MAT_FILE_LEN) { + cfclose(cfp); + mat_warning(SCP_string(filename) + " is empty or larger than " + std::to_string(MAX_MAT_FILE_LEN) + + " bytes. Ignoring it."); + return false; + } + + SCP_vector text(static_cast(file_len) + 1, '\0'); + const int read = cfread(text.data(), file_len, 1, cfp); + cfclose(cfp); + if (read != 1) { + mat_warning(SCP_string("Could not read ") + filename + ". Ignoring it."); + return false; + } + + const size_t start = (file_len >= 3 && memcmp(text.data(), "\xEF\xBB\xBF", 3) == 0) ? 3 : 0; + if (!is_utf8_text(text, start, static_cast(file_len))) { + mat_warning(SCP_string(filename) + " is not UTF-8 text. Ignoring it; save it as UTF-8 to use it."); + return false; + } + + // a texture can load while a table is being parsed, so the parse position and counters are saved around this one; + // warnings wait until it is restored, since a debug build can exit from the warning dialog + SCP_vector warnings; + pause_parse(); + try { + parse_texture_material(filename, text.data() + start, mat, warnings); + } catch (...) { + unpause_parse(); + throw; + } + unpause_parse(); + + for (const auto& warning : warnings) + mat_warning(warning); + + return true; +} + +} // namespace + +const texture_material* texture_material_find(const char* texture_filename, int dir_type) +{ + if (texture_filename == nullptr || *texture_filename == '\0') + return nullptr; + + SCP_string name = texture_filename; + const auto dot = name.rfind('.'); + if (dot != SCP_string::npos) + name.erase(dot); + name += ".mat"; + + const auto key = name + '|' + std::to_string(dir_type); + const auto found = Texture_materials.find(key); + if (found != Texture_materials.end()) + return found->second ? &*found->second : nullptr; + + auto& entry = Texture_materials[key]; + if (!cf_exists_full(name.c_str(), dir_type)) + return nullptr; + + texture_material mat; + if (!read_texture_material(name.c_str(), dir_type, mat)) + return nullptr; + + mprintf(("TEXTURE MATERIAL: Using '%s'\n", name.c_str())); + entry = mat; + return &*entry; +} + +void texture_material_reset() +{ + Texture_materials.clear(); +} + +float texture_glow_intensity(const texture_glow_settings& glow) +{ + return glow.intensity.value_or(lighting_profiles::current_glow_map_intensity()); +} + +float texture_glow_hdr_multiplier(const texture_glow_settings& glow) +{ + return glow.hdr_multiplier.value_or( + lighting_profiles::current_glow_map_hdr_multiplier(texture_glow_intensity(glow))); +} diff --git a/code/graphics/texture_material.h b/code/graphics/texture_material.h new file mode 100644 index 00000000000..28d7437c409 --- /dev/null +++ b/code/graphics/texture_material.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +struct texture_glow_settings { + std::optional intensity; + std::optional hdr_multiplier; +}; + +struct texture_material { + texture_glow_settings glow; +}; + +/** + * @brief Finds and parses the .mat file that belongs to a texture + * + * Each name is read once per session, so edits to a .mat file take effect after a restart. + * + * @param texture_filename The texture's filename; any extension is ignored + * @param dir_type The CF_TYPE_ the texture was requested from + * + * @returns The parsed settings, valid until texture_material_reset(), or nullptr if the texture has no usable .mat file + */ +const texture_material* texture_material_find(const char* texture_filename, int dir_type); + +/** + * @brief Forgets every .mat file read so far + * + * Only for when no bitmap is loaded, such as between unit tests, since loaded bitmaps point into this storage. + */ +void texture_material_reset(); + +// A texture's glow values, falling back to the current lighting profile for any it doesn't set +float texture_glow_intensity(const texture_glow_settings& glow); +float texture_glow_hdr_multiplier(const texture_glow_settings& glow); diff --git a/code/graphics/uniforms.cpp b/code/graphics/uniforms.cpp index 15ddb93a7dc..5f20ca0de5f 100644 --- a/code/graphics/uniforms.cpp +++ b/code/graphics/uniforms.cpp @@ -222,6 +222,9 @@ void convert_model_material(model_uniform_data* data_out, if (material.is_alpha_mult_active()) { data_out->alphaMult = material.get_alpha_mult(); } + + data_out->glowIntensity = material.get_glow_intensity(); + data_out->glowHdrMultiplier = material.get_glow_hdr_multiplier(); } void convert_shadow_material(shadow_uniform_data* data_out, diff --git a/code/graphics/util/uniform_structs.h b/code/graphics/util/uniform_structs.h index ab622172506..e3edcef5bc6 100644 --- a/code/graphics/util/uniform_structs.h +++ b/code/graphics/util/uniform_structs.h @@ -123,8 +123,13 @@ struct model_uniform_data { int sMiscmapIndex; float alphaMult; int flags; - float pad; + float glowIntensity; + + float glowHdrMultiplier; + float pad[3]; }; +static_assert(offsetof(model_uniform_data, glowHdrMultiplier) == 1056 && sizeof(model_uniform_data) == 1072, + "model_uniform_data must match the GLSL modelData layout"); const size_t model_uniform_data_size = sizeof(model_uniform_data); const float mud_align = model_uniform_data_size / 16.0f; @@ -228,8 +233,12 @@ struct decal_info { int diffuse_blend_mode; int glow_blend_mode; - float pad[3]; + float glow_intensity; + float glow_hdr_multiplier; + float pad; }; +static_assert(offsetof(decal_info, glow_hdr_multiplier) == 24 && sizeof(decal_info) == 32, + "decal_info must match the GLSL decalInfoData layout"); struct matrix_uniforms { matrix4 modelViewMatrix; diff --git a/code/lighting/lighting_profiles.cpp b/code/lighting/lighting_profiles.cpp index d0f4c542ccd..aa00ecd2874 100644 --- a/code/lighting/lighting_profiles.cpp +++ b/code/lighting/lighting_profiles.cpp @@ -15,6 +15,8 @@ #include "parse/parsehi.h" #include "parse/parselo.h" +#include + namespace lighting_profiles { typedef int profile_index; //************************************************* @@ -97,6 +99,40 @@ float current_exposure() return _current.exposure; } +namespace { +std::optional checked_glow_value(float value, const char* key, const SCP_string& profile_name) +{ + if (!std::isfinite(value)) { + Warning(LOCATION, "%s in lighting profile %s needs a number. Ignoring it.", key, profile_name.c_str()); + return std::nullopt; + } + if (value < 0.0f) { + Warning(LOCATION, "%s in lighting profile %s is negative; using 0.", key, profile_name.c_str()); + return 0.0f; + } + if (value > MAX_GLOW_MAP_VALUE) { + Warning(LOCATION, + "%s in lighting profile %s is above %d; using that.", + key, + profile_name.c_str(), + static_cast(MAX_GLOW_MAP_VALUE)); + return MAX_GLOW_MAP_VALUE; + } + return value; +} +} // namespace + +float current_glow_map_intensity() +{ + return _current.glow_map_intensity; +} + +float current_glow_map_hdr_multiplier(float intensity) +{ + // unless set, the HDR multiplier follows the intensity, keeping the old 1.5 x 3.0 by default + return _current.glow_map_hdr_multiplier.value_or(2.0f * intensity); +} + piecewise_power_curve_intermediates current_piecewise_intermediates() { return calc_intermediates(_current.ppc_values); @@ -423,6 +459,18 @@ void profile::parse(const char* filename, const SCP_string& profile_name, const profile_name, &cockpit_light_intensity_modifier); + float glow_value; + if (parse_optional_float_into("$Glow Map Intensity:", &glow_value)) { + parsed = true; + if (auto checked = checked_glow_value(glow_value, "$Glow Map Intensity:", profile_name)) + glow_map_intensity = *checked; + } + if (parse_optional_float_into("$Glow Map HDR Multiplier:", &glow_value)) { + parsed = true; + if (auto checked = checked_glow_value(glow_value, "$Glow Map HDR Multiplier:", profile_name)) + glow_map_hdr_multiplier = *checked; + } + if (!parsed) { stuff_string(buffer, F_RAW); Warning(LOCATION, "Unhandled line in lighting profile\n\t%s", buffer.c_str()); @@ -444,6 +492,9 @@ void profile::reset() exposure = 4.0f; + glow_map_intensity = 1.5f; + glow_map_hdr_multiplier.reset(); + missile_light_brightness.reset(); missile_light_radius.reset(); missile_light_radius.base = 20.0f; diff --git a/code/lighting/lighting_profiles.h b/code/lighting/lighting_profiles.h index 92819615bdf..29094c03ab8 100644 --- a/code/lighting/lighting_profiles.h +++ b/code/lighting/lighting_profiles.h @@ -4,6 +4,8 @@ #include "globalincs/adjustment.h" #include "globalincs/vmallocator.h" +#include + namespace lighting_profiles { // This file handles the profiles.tbl and -ltp.tbm files. The purpose of these files is to provide control over the HDR @@ -78,6 +80,8 @@ class profile { adjustment overall_brightness; adjustment cockpit_light_radius_modifier; adjustment cockpit_light_intensity_modifier; + float glow_map_intensity; + std::optional glow_map_hdr_multiplier; void reset(); profile& operator=(const profile& rhs); @@ -97,6 +101,11 @@ const piecewise_power_curve_values& current_piecewise_values(); piecewise_power_curve_intermediates current_piecewise_intermediates(); piecewise_power_curve_intermediates calc_intermediates(piecewise_power_curve_values input); float current_exposure(); +// keeps glow within the 16-bit float emissive buffer: 100 x a default HDR multiplier of 200 is 20,000, under 65,504 +constexpr float MAX_GLOW_MAP_VALUE = 100.0f; + +float current_glow_map_intensity(); +float current_glow_map_hdr_multiplier(float intensity); void lab_set_exposure(float exIn); void lab_set_tonemapper(TonemapperAlgorithm tnin); void lab_set_ppc(const piecewise_power_curve_values &ppcin); diff --git a/code/model/modelrender.cpp b/code/model/modelrender.cpp index a9a772cf652..d08a6b13353 100644 --- a/code/model/modelrender.cpp +++ b/code/model/modelrender.cpp @@ -1065,8 +1065,11 @@ void model_render_buffers(model_draw_list* scene, model_material *rendering_mate texture_maps[TM_SPEC_GLOSS_TYPE] = -1; texture_maps[TM_AMBIENT_TYPE] = -1; + int material_base = -1; + if (forced_texture != -2) { texture_maps[TM_BASE_TYPE] = forced_texture; + material_base = forced_texture; alpha = forced_alpha; if (interp->get_warp_bitmap() >= 0) { @@ -1094,6 +1097,9 @@ void model_render_buffers(model_draw_list* scene, model_material *rendering_mate continue; } + // the .mat still applies to the glow map when the lab hides the diffuse map + material_base = texture_maps[TM_BASE_TYPE]; + // doing glow maps? if ( !(model_flags & MR_NO_GLOWMAPS) ) { auto tglow = &tmap->textures[TM_GLOW_TYPE]; @@ -1216,6 +1222,9 @@ void model_render_buffers(model_draw_list* scene, model_material *rendering_mate model_render_determine_color(&clr, alpha, blend_mode, no_texturing ? true : false, rendering_material->is_desaturated()); rendering_material->set_color(clr); + const auto mat = bm_get_texture_material(material_base); + rendering_material->set_glow(mat != nullptr ? mat->glow : texture_glow_settings()); + if ( (tmap_flags & TMAP_FLAG_TEXTURED) && (buffer->flags & VB_FLAG_UV1) ) { rendering_material->set_texture_map(TM_BASE_TYPE, texture_maps[TM_BASE_TYPE]); diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 04734bca905..1f893b546ef 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -483,6 +483,8 @@ add_file_folder("Graphics" graphics/render_queue.h graphics/shadows.cpp graphics/shadows.h + graphics/texture_material.cpp + graphics/texture_material.h graphics/tmapper.h graphics/uniforms.cpp graphics/uniforms.h diff --git a/test/src/cfile/cfile.cpp b/test/src/cfile/cfile.cpp index f9842a55081..86574838f70 100644 --- a/test/src/cfile/cfile.cpp +++ b/test/src/cfile/cfile.cpp @@ -1,4 +1,5 @@ +#include #include #include #include @@ -212,3 +213,119 @@ TEST_F(CFileTest, subfolder_list) ASSERT_EQ(2, cf_get_file_list(table_files, CF_TYPE_TABLES, "*\\*.tbl", CF_SORT_NAME)); ASSERT_TRUE(table_files.back().substr(0, 6) == "folder"); } + +namespace { +// Builds the bytes cfwrite_string_len writes (a little-endian length, then the characters) as an in-memory file. +// The file reads from this object's buffer, so the object has to outlive the CFILE. +class StringLenFile { + public: + StringLenFile& add_int(int value) + { + for (int i = 0; i < 4; ++i) { + _bytes.push_back(static_cast((value >> (8 * i)) & 0xFF)); + } + return *this; + } + + StringLenFile& add_chars(const char* chars, size_t count) + { + _bytes.insert(_bytes.end(), chars, chars + count); + return *this; + } + + CFILE* open() const + { + CFileLocation res(true); + res.name_ext = "string_len.bin"; + res.full_name = "string_len.bin"; + res.data_ptr = _bytes.data(); + res.size = _bytes.size(); + return cfopen_special(res, "rb"); + } + + private: + SCP_vector _bytes; +}; +} // namespace + +TEST_F(CFileTest, string_len_fits) +{ + StringLenFile file; + file.add_int(3).add_chars("abc", 3).add_int(42); + auto fp = file.open(); + ASSERT_NE(fp, nullptr); + + char buf[8]; + cfread_string_len(buf, sizeof(buf), fp); + EXPECT_STREQ(buf, "abc"); + EXPECT_EQ(cfread_int(fp), 42); + + cfclose(fp); +} + +TEST_F(CFileTest, string_len_exact_fit_with_stored_terminator) +{ + StringLenFile file; + file.add_int(4).add_chars("abc\0", 4).add_int(42); + auto fp = file.open(); + ASSERT_NE(fp, nullptr); + + char buf[4]; + cfread_string_len(buf, sizeof(buf), fp); + EXPECT_STREQ(buf, "abc"); + EXPECT_EQ(cfread_int(fp), 42); + + cfclose(fp); +} + +TEST_F(CFileTest, string_len_longer_than_buffer) +{ + StringLenFile file; + file.add_int(10).add_chars("0123456789", 10).add_int(42); + auto fp = file.open(); + ASSERT_NE(fp, nullptr); + + char buf[8]; + memset(buf, 'X', sizeof(buf)); + cfread_string_len(buf, 4, fp); + EXPECT_STREQ(buf, "012"); + for (int i = 4; i < 8; ++i) { + EXPECT_EQ(buf[i], 'X') << "wrote past the buffer at " << i; + } + EXPECT_EQ(cfread_int(fp), 42); + + cfclose(fp); +} + +TEST_F(CFileTest, string_len_zero_and_negative) +{ + StringLenFile file; + file.add_int(0).add_int(-5).add_int(42); + auto fp = file.open(); + ASSERT_NE(fp, nullptr); + + char buf[8] = "old"; + cfread_string_len(buf, sizeof(buf), fp); + EXPECT_STREQ(buf, ""); + + strcpy_s(buf, "old"); + cfread_string_len(buf, sizeof(buf), fp); + EXPECT_STREQ(buf, ""); + EXPECT_EQ(cfread_int(fp), 42); + + cfclose(fp); +} + +TEST_F(CFileTest, string_len_scp_string) +{ + StringLenFile file; + file.add_int(3).add_chars("abc", 3).add_int(-5).add_int(1000000).add_chars("de", 2); + auto fp = file.open(); + ASSERT_NE(fp, nullptr); + + EXPECT_EQ(cfread_string_len(fp), "abc"); + EXPECT_EQ(cfread_string_len(fp), ""); + EXPECT_EQ(cfread_string_len(fp), "de"); + + cfclose(fp); +} diff --git a/test/src/graphics/test_texture_material.cpp b/test/src/graphics/test_texture_material.cpp new file mode 100644 index 00000000000..cea8d6ab582 --- /dev/null +++ b/test/src/graphics/test_texture_material.cpp @@ -0,0 +1,272 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "util/FSTestFixture.h" + +namespace { + +// Debug test builds turn Warning() into an exception, Release builds only count it +void expect_warning(const char* name, int dir_type, const char* text) +{ +#ifndef NDEBUG + try { + texture_material_find(name, dir_type); + FAIL() << "expected a warning containing '" << text << "'"; + } catch (const os::dialogs::WarningException& e) { + EXPECT_NE(SCP_string(e.what()).find(text), SCP_string::npos) << e.what(); + } +#else + int warnings = Global_warning_count; + texture_material_find(name, dir_type); + EXPECT_EQ(Global_warning_count, warnings + 1) << "expected a warning containing '" << text << "'"; +#endif +} + +} // namespace + +class TextureMaterialTest : public test::FSTestFixture { +public: + TextureMaterialTest() : test::FSTestFixture(INIT_CFILE) { + pushModDir("graphics"); + pushModDir("texture_material"); + } + +protected: + void TearDown() override { + texture_material_reset(); + test::FSTestFixture::TearDown(); + } +}; + +class TextureMaterialBitmapTest : public test::FSTestFixture { +public: + TextureMaterialBitmapTest() : test::FSTestFixture(INIT_CFILE | INIT_GRAPHICS) { + pushModDir("graphics"); + pushModDir("texture_material"); + } + +protected: + // the fixture runs as a standalone server, where bmpman swaps every name for a placeholder + void SetUp() override { + test::FSTestFixture::SetUp(); + _was_standalone = Is_standalone; + Is_standalone = 0; + } + + void TearDown() override { + Is_standalone = _was_standalone; + test::FSTestFixture::TearDown(); + texture_material_reset(); + } + +private: + int _was_standalone = 0; +}; + +TEST_F(TextureMaterialTest, reads_both_glow_values) { + auto mat = texture_material_find("mat_glow", CF_TYPE_MAPS); + ASSERT_NE(mat, nullptr); + EXPECT_FLOAT_EQ(*mat->glow.intensity, 2.5f); + EXPECT_FLOAT_EQ(*mat->glow.hdr_multiplier, 6.0f); +} + +TEST_F(TextureMaterialTest, unset_values_stay_unset_and_comments_are_skipped) { + auto mat = texture_material_find("mat_intensity_only", CF_TYPE_MAPS); + ASSERT_NE(mat, nullptr); + EXPECT_FLOAT_EQ(*mat->glow.intensity, 0.75f); + EXPECT_FALSE(mat->glow.hdr_multiplier.has_value()); + + auto inline_comment = texture_material_find("mat_inline_comment", CF_TYPE_MAPS); + ASSERT_NE(inline_comment, nullptr); + EXPECT_FLOAT_EQ(*inline_comment->glow.intensity, 1.5f); +} + +TEST_F(TextureMaterialTest, crlf_and_utf8_bom_are_read) { + auto crlf = texture_material_find("mat_crlf", CF_TYPE_MAPS); + ASSERT_NE(crlf, nullptr); + EXPECT_FLOAT_EQ(*crlf->glow.intensity, 0.5f); + EXPECT_FLOAT_EQ(*crlf->glow.hdr_multiplier, 2.25f); + + auto bom = texture_material_find("mat_bom", CF_TYPE_MAPS); + ASSERT_NE(bom, nullptr); + EXPECT_FLOAT_EQ(*bom->glow.intensity, 1.75f); +} + +TEST_F(TextureMaterialTest, utf8_comments_and_tabs_are_read) { + int warnings = Global_warning_count; + auto mat = texture_material_find("mat_utf8", CF_TYPE_MAPS); + ASSERT_NE(mat, nullptr); + EXPECT_FLOAT_EQ(*mat->glow.intensity, 3.5f); + + auto tab = texture_material_find("mat_tab", CF_TYPE_MAPS); + ASSERT_NE(tab, nullptr); + EXPECT_FLOAT_EQ(*tab->glow.intensity, 1.5f); + EXPECT_EQ(Global_warning_count, warnings); +} + +TEST_F(TextureMaterialTest, texture_without_a_mat_file) { + EXPECT_EQ(texture_material_find("mat_plain", CF_TYPE_MAPS), nullptr); + EXPECT_EQ(texture_material_find("", CF_TYPE_MAPS), nullptr); + EXPECT_EQ(texture_material_find(nullptr, CF_TYPE_MAPS), nullptr); +} + +TEST_F(TextureMaterialTest, extension_and_case_are_ignored) { + auto upper = texture_material_find("MAT_GLOW.png", CF_TYPE_MAPS); + ASSERT_NE(upper, nullptr); + EXPECT_EQ(texture_material_find("mat_glow", CF_TYPE_MAPS), upper); + EXPECT_EQ(texture_material_find("mat_glow.dds", CF_TYPE_MAPS), upper); +} + +TEST_F(TextureMaterialTest, directory_type_is_part_of_the_lookup) { + auto any = texture_material_find("mat_fx", CF_TYPE_ANY); + ASSERT_NE(any, nullptr); + EXPECT_FLOAT_EQ(*any->glow.intensity, 6.0f); + EXPECT_EQ(texture_material_find("mat_fx", CF_TYPE_MAPS), nullptr); + EXPECT_NE(texture_material_find("mat_fx", CF_TYPE_EFFECTS), nullptr); +} + +TEST_F(TextureMaterialTest, non_text_files_are_rejected) { + expect_warning("mat_sharp_s", CF_TYPE_MAPS, "not UTF-8 text"); + expect_warning("mat_utf16", CF_TYPE_MAPS, "not UTF-8 text"); + expect_warning("mat_utf16_nobom", CF_TYPE_MAPS, "not UTF-8 text"); + expect_warning("mat_encrypted", CF_TYPE_MAPS, "not UTF-8 text"); + expect_warning("mat_ctrl", CF_TYPE_MAPS, "not UTF-8 text"); + expect_warning("mat_del", CF_TYPE_MAPS, "not UTF-8 text"); + expect_warning("mat_truncated", CF_TYPE_MAPS, "not UTF-8 text"); + expect_warning("mat_empty", CF_TYPE_MAPS, "is empty"); +#ifdef NDEBUG + for (const char* name : {"mat_sharp_s", "mat_utf16", "mat_utf16_nobom", "mat_encrypted", "mat_ctrl", "mat_del", + "mat_truncated", "mat_empty"}) + EXPECT_EQ(texture_material_find(name, CF_TYPE_MAPS), nullptr) << name; +#endif +} + +TEST_F(TextureMaterialTest, a_rejected_file_warns_once) { + expect_warning("mat_sharp_s", CF_TYPE_MAPS, "not UTF-8 text"); +#ifdef NDEBUG + int warnings = Global_warning_count; + EXPECT_EQ(texture_material_find("mat_sharp_s", CF_TYPE_MAPS), nullptr); + EXPECT_EQ(Global_warning_count, warnings); +#endif +} + +TEST_F(TextureMaterialTest, bad_values_warn_and_are_ignored) { + expect_warning("mat_bad_number", CF_TYPE_MAPS, "needs a number"); + expect_warning("mat_nan", CF_TYPE_MAPS, "needs a number"); + expect_warning("mat_no_value", CF_TYPE_MAPS, "needs a number"); +#ifdef NDEBUG + EXPECT_FALSE(texture_material_find("mat_bad_number", CF_TYPE_MAPS)->glow.intensity.has_value()); + EXPECT_FALSE(texture_material_find("mat_nan", CF_TYPE_MAPS)->glow.intensity.has_value()); + auto no_value = texture_material_find("mat_no_value", CF_TYPE_MAPS); + EXPECT_FALSE(no_value->glow.intensity.has_value()); + EXPECT_FLOAT_EQ(*no_value->glow.hdr_multiplier, 4.0f); +#endif +} + +TEST_F(TextureMaterialTest, out_of_range_values_are_clamped) { + expect_warning("mat_negative", CF_TYPE_MAPS, "negative"); + expect_warning("mat_huge", CF_TYPE_MAPS, "above"); +#ifdef NDEBUG + EXPECT_FLOAT_EQ(*texture_material_find("mat_negative", CF_TYPE_MAPS)->glow.intensity, 0.0f); + EXPECT_FLOAT_EQ(*texture_material_find("mat_huge", CF_TYPE_MAPS)->glow.intensity, + lighting_profiles::MAX_GLOW_MAP_VALUE); +#endif +} + +TEST_F(TextureMaterialTest, extra_text_and_unknown_entries_warn_and_keep_the_rest) { + expect_warning("mat_trailing", CF_TYPE_MAPS, "extra text"); + expect_warning("mat_unknown_key", CF_TYPE_MAPS, "unrecognized"); +#ifdef NDEBUG + EXPECT_FLOAT_EQ(*texture_material_find("mat_trailing", CF_TYPE_MAPS)->glow.intensity, 2.0f); + EXPECT_FLOAT_EQ(*texture_material_find("mat_unknown_key", CF_TYPE_MAPS)->glow.intensity, 1.25f); +#endif +} + +TEST_F(TextureMaterialTest, the_largest_values_fit_the_emissive_buffer) { + // the emissive render target is 16-bit float, whose largest finite value is 65504 + texture_glow_settings glow; + glow.intensity = lighting_profiles::MAX_GLOW_MAP_VALUE; + EXPECT_LE(texture_glow_intensity(glow) * texture_glow_hdr_multiplier(glow), 65504.0f); +} + +TEST_F(TextureMaterialTest, many_unknown_entries_warn_once_and_are_counted) { +#ifndef NDEBUG + EXPECT_THROW(texture_material_find("mat_many_unknown", CF_TYPE_MAPS), os::dialogs::WarningException); +#else + // one warning for the first unknown entry, one summary for the other three + int warnings = Global_warning_count; + auto mat = texture_material_find("mat_many_unknown", CF_TYPE_MAPS); + EXPECT_EQ(Global_warning_count, warnings + 2); + ASSERT_NE(mat, nullptr); + EXPECT_FLOAT_EQ(*mat->glow.intensity, 1.25f); +#endif +} + +TEST_F(TextureMaterialTest, an_outer_parse_resumes_after_a_mat_file) { +#ifndef NDEBUG + EXPECT_THROW(texture_material_find("mat_bad_number", CF_TYPE_MAPS), os::dialogs::WarningException); + // a parse left paused makes this read fail + EXPECT_NO_THROW(read_file_text("mat_glow.mat", CF_TYPE_MAPS)); +#endif + char outer[] = "$Outer: 1\n"; + reset_parse(outer); + pause_parse(); + const texture_material* mat = nullptr; + try { + mat = texture_material_find("mat_intensity_only", CF_TYPE_MAPS); + } catch (...) { + unpause_parse(); + throw; + } + unpause_parse(); + EXPECT_NE(mat, nullptr); + EXPECT_EQ(Mp, outer); +} + +TEST_F(TextureMaterialBitmapTest, loaded_texture_carries_its_material) { + int textured = bm_load("mat_textured", CF_TYPE_MAPS); + ASSERT_GE(textured, 0); + auto mat = bm_get_texture_material(textured); + ASSERT_NE(mat, nullptr); + EXPECT_FLOAT_EQ(*mat->glow.intensity, 4.0f); + + int plain = bm_load("mat_plain", CF_TYPE_MAPS); + ASSERT_GE(plain, 0); + EXPECT_EQ(bm_get_texture_material(plain), nullptr); + + EXPECT_EQ(bm_get_texture_material(-1), nullptr); +} + +TEST_F(TextureMaterialBitmapTest, eff_frames_share_the_material) { + int nframes = 0; + int first = bm_load_animation("mat_anim", &nframes, nullptr, nullptr, nullptr, false, CF_TYPE_MAPS); + ASSERT_GE(first, 0); + ASSERT_EQ(nframes, 2); + + for (int i = 0; i < nframes; i++) { + auto mat = bm_get_texture_material(first + i); + ASSERT_NE(mat, nullptr); + EXPECT_FLOAT_EQ(*mat->glow.hdr_multiplier, 8.0f); + } +} + +TEST_F(TextureMaterialBitmapTest, apng_frames_share_the_material) { + int nframes = 0; + int first = bm_load_animation("mat_apng", &nframes, nullptr, nullptr, nullptr, false, CF_TYPE_MAPS); + ASSERT_GE(first, 0); + ASSERT_EQ(nframes, 2); + + for (int i = 0; i < nframes; i++) { + auto mat = bm_get_texture_material(first + i); + ASSERT_NE(mat, nullptr); + EXPECT_FLOAT_EQ(*mat->glow.intensity, 9.0f); + } +} diff --git a/test/src/lighting/test_glow_settings.cpp b/test/src/lighting/test_glow_settings.cpp new file mode 100644 index 00000000000..34de27efb28 --- /dev/null +++ b/test/src/lighting/test_glow_settings.cpp @@ -0,0 +1,122 @@ +#include +#include +#include + +#include "util/FSTestFixture.h" + +static float profile_hdr_multiplier() +{ + return lighting_profiles::current_glow_map_hdr_multiplier(lighting_profiles::current_glow_map_intensity()); +} + +class GlowSettingsTest : public test::FSTestFixture { +public: + GlowSettingsTest() : test::FSTestFixture(INIT_CFILE) { + pushModDir("lighting"); + } + +protected: + void SetUp() override { + test::FSTestFixture::SetUp(); + lighting_profiles::load_profiles(); + } + + void TearDown() override { + lighting_profiles::switch_to(lighting_profiles::default_name()); + test::FSTestFixture::TearDown(); + } +}; + +TEST_F(GlowSettingsTest, defaults_match_the_old_shader_constants) { + lighting_profiles::switch_to("Glow Default"); + ASSERT_EQ(lighting_profiles::current()->name, "Glow Default"); + EXPECT_FLOAT_EQ(lighting_profiles::current_glow_map_intensity(), 1.5f); + EXPECT_FLOAT_EQ(profile_hdr_multiplier(), 3.0f); +} + +TEST_F(GlowSettingsTest, hdr_multiplier_follows_intensity_unless_set) { + lighting_profiles::switch_to("Glow Intensity"); + EXPECT_FLOAT_EQ(lighting_profiles::current_glow_map_intensity(), 2.0f); + EXPECT_FLOAT_EQ(profile_hdr_multiplier(), 4.0f); + + lighting_profiles::switch_to("Glow Both"); + EXPECT_FLOAT_EQ(lighting_profiles::current_glow_map_intensity(), 2.0f); + EXPECT_FLOAT_EQ(profile_hdr_multiplier(), 5.0f); + + lighting_profiles::switch_to("Glow Intensity"); + EXPECT_FLOAT_EQ(profile_hdr_multiplier(), 4.0f); +} + +TEST_F(GlowSettingsTest, texture_values_override_the_profile) { + lighting_profiles::switch_to("Glow Both"); + model_material material; + EXPECT_FLOAT_EQ(material.get_glow_intensity(), 2.0f); + EXPECT_FLOAT_EQ(material.get_glow_hdr_multiplier(), 5.0f); + + material.set_glow({3.0f, std::nullopt}); + EXPECT_FLOAT_EQ(material.get_glow_intensity(), 3.0f); + EXPECT_FLOAT_EQ(material.get_glow_hdr_multiplier(), 5.0f); + + material.set_glow({std::nullopt, 7.0f}); + EXPECT_FLOAT_EQ(material.get_glow_intensity(), 2.0f); + EXPECT_FLOAT_EQ(material.get_glow_hdr_multiplier(), 7.0f); + + material.set_glow({}); + EXPECT_FLOAT_EQ(material.get_glow_intensity(), 2.0f); + EXPECT_FLOAT_EQ(material.get_glow_hdr_multiplier(), 5.0f); + + lighting_profiles::switch_to("Glow Intensity"); + material.set_glow({3.0f, std::nullopt}); + EXPECT_FLOAT_EQ(material.get_glow_hdr_multiplier(), 6.0f); +} + +class GlowNegativeProfileTest : public test::FSTestFixture { +public: + GlowNegativeProfileTest() : test::FSTestFixture(INIT_CFILE) { + pushModDir("lighting"); + } + +protected: + void TearDown() override { +#ifdef NDEBUG + lighting_profiles::switch_to(lighting_profiles::default_name()); +#endif + test::FSTestFixture::TearDown(); + } +}; + +TEST_F(GlowNegativeProfileTest, negative_glow_values_clamp_to_zero) { +#ifndef NDEBUG + GTEST_SKIP() << "in debug test builds Warning() throws mid-parse"; +#else + int warnings = Global_warning_count; + lighting_profiles::load_profiles(); + EXPECT_EQ(Global_warning_count, warnings + 2); + + lighting_profiles::switch_to("Glow Negative"); + EXPECT_FLOAT_EQ(lighting_profiles::current_glow_map_intensity(), 0.0f); + EXPECT_FLOAT_EQ(profile_hdr_multiplier(), 0.0f); + + lighting_profiles::switch_to("Glow Negative HDR"); + EXPECT_FLOAT_EQ(lighting_profiles::current_glow_map_intensity(), 1.5f); + EXPECT_FLOAT_EQ(profile_hdr_multiplier(), 0.0f); +#endif +} + +TEST_F(GlowNegativeProfileTest, bad_glow_values_are_rejected) { +#ifndef NDEBUG + GTEST_SKIP() << "in debug test builds Warning() throws mid-parse"; +#else + int warnings = Global_warning_count; + lighting_profiles::load_profiles(); + EXPECT_EQ(Global_warning_count, warnings + 4); + + lighting_profiles::switch_to("Glow NaN"); + EXPECT_FLOAT_EQ(lighting_profiles::current_glow_map_intensity(), 1.5f); + EXPECT_FLOAT_EQ(profile_hdr_multiplier(), 3.0f); + + lighting_profiles::switch_to("Glow Huge"); + EXPECT_FLOAT_EQ(lighting_profiles::current_glow_map_intensity(), lighting_profiles::MAX_GLOW_MAP_VALUE); + EXPECT_FLOAT_EQ(profile_hdr_multiplier(), lighting_profiles::MAX_GLOW_MAP_VALUE); +#endif +} diff --git a/test/src/source_groups.cmake b/test/src/source_groups.cmake index 1c268dda86e..3bee637e5dc 100644 --- a/test/src/source_groups.cmake +++ b/test/src/source_groups.cmake @@ -25,6 +25,7 @@ add_file_folder("Globalincs" add_file_folder("Graphics" graphics/test_font.cpp + graphics/test_texture_material.cpp ) if (FSO_BUILD_WITH_VULKAN) @@ -33,6 +34,10 @@ if (FSO_BUILD_WITH_VULKAN) ) endif() +add_file_folder("Lighting" + lighting/test_glow_settings.cpp +) + add_file_folder("Math" math/test_vecmat.cpp ) diff --git a/test/test_data/graphics/texture_material/data/effects/mat_fx.mat b/test/test_data/graphics/texture_material/data/effects/mat_fx.mat new file mode 100644 index 00000000000..50c3a42ab45 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/effects/mat_fx.mat @@ -0,0 +1 @@ +$Glow Map Intensity: 6 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_anim.eff b/test/test_data/graphics/texture_material/data/maps/mat_anim.eff new file mode 100644 index 00000000000..bfa7706c289 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_anim.eff @@ -0,0 +1,3 @@ +$Type: png +$Frames: 2 +$FPS: 10 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_anim.mat b/test/test_data/graphics/texture_material/data/maps/mat_anim.mat new file mode 100644 index 00000000000..1310b9a38fb --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_anim.mat @@ -0,0 +1 @@ +$Glow Map HDR Multiplier: 8 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_anim_0000.png b/test/test_data/graphics/texture_material/data/maps/mat_anim_0000.png new file mode 100644 index 00000000000..0dbabee15ef Binary files /dev/null and b/test/test_data/graphics/texture_material/data/maps/mat_anim_0000.png differ diff --git a/test/test_data/graphics/texture_material/data/maps/mat_anim_0001.png b/test/test_data/graphics/texture_material/data/maps/mat_anim_0001.png new file mode 100644 index 00000000000..3a61ea5b435 Binary files /dev/null and b/test/test_data/graphics/texture_material/data/maps/mat_anim_0001.png differ diff --git a/test/test_data/graphics/texture_material/data/maps/mat_apng.mat b/test/test_data/graphics/texture_material/data/maps/mat_apng.mat new file mode 100644 index 00000000000..dfccf8d1146 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_apng.mat @@ -0,0 +1 @@ +$Glow Map Intensity: 9 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_apng.png b/test/test_data/graphics/texture_material/data/maps/mat_apng.png new file mode 100644 index 00000000000..4773cbcba49 Binary files /dev/null and b/test/test_data/graphics/texture_material/data/maps/mat_apng.png differ diff --git a/test/test_data/graphics/texture_material/data/maps/mat_bad_number.mat b/test/test_data/graphics/texture_material/data/maps/mat_bad_number.mat new file mode 100644 index 00000000000..23779684a93 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_bad_number.mat @@ -0,0 +1 @@ +$Glow Map Intensity: bright diff --git a/test/test_data/graphics/texture_material/data/maps/mat_bom.mat b/test/test_data/graphics/texture_material/data/maps/mat_bom.mat new file mode 100644 index 00000000000..4bca6326a8b --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_bom.mat @@ -0,0 +1 @@ +$Glow Map Intensity: 1.75 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_crlf.mat b/test/test_data/graphics/texture_material/data/maps/mat_crlf.mat new file mode 100644 index 00000000000..c761b87e7fc --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_crlf.mat @@ -0,0 +1,2 @@ +$Glow Map Intensity: 0.5 +$Glow Map HDR Multiplier: 2.25 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_ctrl.mat b/test/test_data/graphics/texture_material/data/maps/mat_ctrl.mat new file mode 100644 index 00000000000..6cb177498d7 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_ctrl.mat @@ -0,0 +1,2 @@ +$Glow Map Intensity: 2 +; ab diff --git a/test/test_data/graphics/texture_material/data/maps/mat_del.mat b/test/test_data/graphics/texture_material/data/maps/mat_del.mat new file mode 100644 index 00000000000..bee4ab086b2 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_del.mat @@ -0,0 +1,2 @@ +$Glow Map Intensity: 2 +; ab diff --git a/test/test_data/graphics/texture_material/data/maps/mat_empty.mat b/test/test_data/graphics/texture_material/data/maps/mat_empty.mat new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_data/graphics/texture_material/data/maps/mat_encrypted.mat b/test/test_data/graphics/texture_material/data/maps/mat_encrypted.mat new file mode 100644 index 00000000000..9b504905529 Binary files /dev/null and b/test/test_data/graphics/texture_material/data/maps/mat_encrypted.mat differ diff --git a/test/test_data/graphics/texture_material/data/maps/mat_glow.mat b/test/test_data/graphics/texture_material/data/maps/mat_glow.mat new file mode 100644 index 00000000000..2c0649d9348 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_glow.mat @@ -0,0 +1,2 @@ +$Glow Map Intensity: 2.5 +$Glow Map HDR Multiplier: 6 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_huge.mat b/test/test_data/graphics/texture_material/data/maps/mat_huge.mat new file mode 100644 index 00000000000..30166d80895 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_huge.mat @@ -0,0 +1 @@ +$Glow Map Intensity: 3e38 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_inline_comment.mat b/test/test_data/graphics/texture_material/data/maps/mat_inline_comment.mat new file mode 100644 index 00000000000..2dc339d5c95 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_inline_comment.mat @@ -0,0 +1 @@ +$Glow Map Intensity: 1.5 ; brighter than the hull diff --git a/test/test_data/graphics/texture_material/data/maps/mat_intensity_only.mat b/test/test_data/graphics/texture_material/data/maps/mat_intensity_only.mat new file mode 100644 index 00000000000..d3939ae4572 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_intensity_only.mat @@ -0,0 +1,2 @@ +; only the intensity is set +$Glow Map Intensity: 0.75 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_many_unknown.mat b/test/test_data/graphics/texture_material/data/maps/mat_many_unknown.mat new file mode 100644 index 00000000000..3e16aa26436 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_many_unknown.mat @@ -0,0 +1,5 @@ +$Shine: 1 +$Gloss: 2 +$Glow Map Intensity: 1.25 +$Spec: 3 +$Rim: 4 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_nan.mat b/test/test_data/graphics/texture_material/data/maps/mat_nan.mat new file mode 100644 index 00000000000..ffe5a3236f2 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_nan.mat @@ -0,0 +1 @@ +$Glow Map Intensity: nan diff --git a/test/test_data/graphics/texture_material/data/maps/mat_negative.mat b/test/test_data/graphics/texture_material/data/maps/mat_negative.mat new file mode 100644 index 00000000000..9d8014f3ec4 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_negative.mat @@ -0,0 +1 @@ +$Glow Map Intensity: -2 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_no_value.mat b/test/test_data/graphics/texture_material/data/maps/mat_no_value.mat new file mode 100644 index 00000000000..25b4b3063d9 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_no_value.mat @@ -0,0 +1,2 @@ +$Glow Map Intensity: +$Glow Map HDR Multiplier: 4 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_plain.png b/test/test_data/graphics/texture_material/data/maps/mat_plain.png new file mode 100644 index 00000000000..03bb96559cd Binary files /dev/null and b/test/test_data/graphics/texture_material/data/maps/mat_plain.png differ diff --git a/test/test_data/graphics/texture_material/data/maps/mat_sharp_s.mat b/test/test_data/graphics/texture_material/data/maps/mat_sharp_s.mat new file mode 100644 index 00000000000..c816be3e21f --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_sharp_s.mat @@ -0,0 +1,2 @@ +$Glow Map Intensity: 2 +; ßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßß diff --git a/test/test_data/graphics/texture_material/data/maps/mat_tab.mat b/test/test_data/graphics/texture_material/data/maps/mat_tab.mat new file mode 100644 index 00000000000..d8c2703ceaa --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_tab.mat @@ -0,0 +1,2 @@ +$Glow Map Intensity: 1.5 + ; note diff --git a/test/test_data/graphics/texture_material/data/maps/mat_textured.mat b/test/test_data/graphics/texture_material/data/maps/mat_textured.mat new file mode 100644 index 00000000000..535854bcb26 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_textured.mat @@ -0,0 +1 @@ +$Glow Map Intensity: 4 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_textured.png b/test/test_data/graphics/texture_material/data/maps/mat_textured.png new file mode 100644 index 00000000000..d7bbffc5d1d Binary files /dev/null and b/test/test_data/graphics/texture_material/data/maps/mat_textured.png differ diff --git a/test/test_data/graphics/texture_material/data/maps/mat_trailing.mat b/test/test_data/graphics/texture_material/data/maps/mat_trailing.mat new file mode 100644 index 00000000000..aa6e70a066e --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_trailing.mat @@ -0,0 +1 @@ +$Glow Map Intensity: 2.0 abc diff --git a/test/test_data/graphics/texture_material/data/maps/mat_truncated.mat b/test/test_data/graphics/texture_material/data/maps/mat_truncated.mat new file mode 100644 index 00000000000..280a8ac0018 --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_truncated.mat @@ -0,0 +1,2 @@ +$Glow Map Intensity: 2 +; à \ No newline at end of file diff --git a/test/test_data/graphics/texture_material/data/maps/mat_unknown_key.mat b/test/test_data/graphics/texture_material/data/maps/mat_unknown_key.mat new file mode 100644 index 00000000000..e0f2c19588b --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_unknown_key.mat @@ -0,0 +1,2 @@ +$Glow Map Intensity: 1.25 +$Specular Boost: 3 diff --git a/test/test_data/graphics/texture_material/data/maps/mat_utf16.mat b/test/test_data/graphics/texture_material/data/maps/mat_utf16.mat new file mode 100644 index 00000000000..aad4b9205ee Binary files /dev/null and b/test/test_data/graphics/texture_material/data/maps/mat_utf16.mat differ diff --git a/test/test_data/graphics/texture_material/data/maps/mat_utf16_nobom.mat b/test/test_data/graphics/texture_material/data/maps/mat_utf16_nobom.mat new file mode 100644 index 00000000000..422dfaa694b Binary files /dev/null and b/test/test_data/graphics/texture_material/data/maps/mat_utf16_nobom.mat differ diff --git a/test/test_data/graphics/texture_material/data/maps/mat_utf8.mat b/test/test_data/graphics/texture_material/data/maps/mat_utf8.mat new file mode 100644 index 00000000000..9191ddcadff --- /dev/null +++ b/test/test_data/graphics/texture_material/data/maps/mat_utf8.mat @@ -0,0 +1,2 @@ +; Glühen für die Schiffe — Kommentar +$Glow Map Intensity: 3.5 ; été diff --git a/test/test_data/lighting/bad_glow_values_are_rejected/data/tables/bad-ltp.tbm b/test/test_data/lighting/bad_glow_values_are_rejected/data/tables/bad-ltp.tbm new file mode 100644 index 00000000000..84a90312daf --- /dev/null +++ b/test/test_data/lighting/bad_glow_values_are_rejected/data/tables/bad-ltp.tbm @@ -0,0 +1,11 @@ +#PROFILES + +$Profile: Glow NaN +$Glow Map Intensity: nan +$Glow Map HDR Multiplier: nan + +$Profile: Glow Huge +$Glow Map Intensity: 500 +$Glow Map HDR Multiplier: 500 + +#END PROFILES diff --git a/test/test_data/lighting/data/tables/glow-ltp.tbm b/test/test_data/lighting/data/tables/glow-ltp.tbm new file mode 100644 index 00000000000..550a9030376 --- /dev/null +++ b/test/test_data/lighting/data/tables/glow-ltp.tbm @@ -0,0 +1,12 @@ +#PROFILES + +$Profile: Glow Default + +$Profile: Glow Intensity +$Glow Map Intensity: 2.0 + +$Profile: Glow Both +$Glow Map Intensity: 2.0 +$Glow Map HDR Multiplier: 5.0 + +#END PROFILES diff --git a/test/test_data/lighting/negative_glow_values_clamp_to_zero/data/tables/neg-ltp.tbm b/test/test_data/lighting/negative_glow_values_clamp_to_zero/data/tables/neg-ltp.tbm new file mode 100644 index 00000000000..1ca10d139fc --- /dev/null +++ b/test/test_data/lighting/negative_glow_values_clamp_to_zero/data/tables/neg-ltp.tbm @@ -0,0 +1,9 @@ +#PROFILES + +$Profile: Glow Negative +$Glow Map Intensity: -1.0 + +$Profile: Glow Negative HDR +$Glow Map HDR Multiplier: -2.0 + +#END PROFILES