diff --git a/3rdparty/Catch2 b/3rdparty/Catch2 index 605a3476..62fd6605 160000 --- a/3rdparty/Catch2 +++ b/3rdparty/Catch2 @@ -1 +1 @@ -Subproject commit 605a34765aa5d5ecbf476b4598a862ada971b0cc +Subproject commit 62fd660583d3ae7a7886930b413c3c570e89786c diff --git a/common/bspfile.cc b/common/bspfile.cc index 8ee4ae17..4c5a0054 100644 --- a/common/bspfile.cc +++ b/common/bspfile.cc @@ -98,7 +98,16 @@ public: } bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const override + int32_t surfflags_from_string(const std::string_view &str) const { + if (string_iequals(str, "special")) { + return TEX_SPECIAL; + } + + return 0; + } + + bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const override { // anything texname other than "hint" in a hint brush is treated as "hintskip", and discarded return !string_iequals(name, "hint"); } @@ -310,8 +319,13 @@ public: } } - bool portal_can_see_through(const contentflags_t &contents0, const contentflags_t &contents1, bool transwater, bool transsky) const override + int32_t contents_from_string(const std::string_view &str) const override { + // Q1 doesn't get contents from files + return 0; + } + + bool portal_can_see_through(const contentflags_t &contents0, const contentflags_t &contents1, bool transwater, bool transsky) const override { /* If water is transparent, liquids are like empty space */ if (transwater) { if (contents_are_liquid(contents0) && contents_are_empty(contents1)) @@ -709,8 +723,21 @@ struct gamedef_q2_t : public gamedef_t return true; } - bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const override + static constexpr const char *surf_bitflag_names[] = {"LIGHT", "SLICK", "SKY", "WARP", "TRANS33", "TRANS66", "FLOWING", "NODRAW", + "HINT" }; + + int32_t surfflags_from_string(const std::string_view &str) const override { + for (size_t i = 0; i < std::size(surf_bitflag_names); i++) { + if (string_iequals(str, surf_bitflag_names[i])) { + return nth_bit(i); + } + } + + return 0; + } + + bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const override { // any face in a hint brush that isn't HINT are treated as "hintskip", and discarded return !(flags.native & Q2_SURF_HINT); } @@ -913,6 +940,22 @@ struct gamedef_q2_t : public gamedef_t return true; } + static constexpr const char *bitflag_names[] = {"SOLID", "WINDOW", "AUX", "LAVA", "SLIME", "WATER", "MIST", "128", + "256", "512", "1024", "2048", "4096", "8192", "16384", "AREAPORTAL", "PLAYERCLIP", "MONSTERCLIP", + "CURRENT_0", "CURRENT_90", "CURRENT_180", "CURRENT_270", "CURRENT_UP", "CURRENT_DOWN", "ORIGIN", "MONSTER", + "DEADMONSTER", "DETAIL", "TRANSLUCENT", "LADDER", "1073741824", "2147483648"}; + + int32_t contents_from_string(const std::string_view &str) const + { + for (size_t i = 0; i < std::size(bitflag_names); i++) { + if (string_iequals(str, bitflag_names[i])) { + return nth_bit(i); + } + } + + return 0; + } + /** * Returns the single content bit of the strongest visible content present */ @@ -1002,11 +1045,6 @@ struct gamedef_q2_t : public gamedef_t return "EMPTY"; } - constexpr const char *bitflag_names[] = {"SOLID", "WINDOW", "AUX", "LAVA", "SLIME", "WATER", "MIST", "128", - "256", "512", "1024", "2048", "4096", "8192", "16384", "AREAPORTAL", "PLAYERCLIP", "MONSTERCLIP", - "CURRENT_0", "CURRENT_90", "CURRENT_180", "CURRENT_270", "CURRENT_UP", "CURRENT_DOWN", "ORIGIN", "MONSTER", - "DEADMONSTER", "DETAIL", "TRANSLUCENT", "LADDER", "1073741824", "2147483648"}; - std::string s; for (int32_t i = 0; i < std::size(bitflag_names); i++) { diff --git a/common/cmdlib.cc b/common/cmdlib.cc index 9e74ab6b..91b17039 100644 --- a/common/cmdlib.cc +++ b/common/cmdlib.cc @@ -78,7 +78,7 @@ string_replaceall(std::string &str, const std::string &from, const std::string & } bool // mxd -string_iequals(const std::string &a, const std::string &b) +string_iequals(const std::string_view &a, const std::string_view &b) { size_t sz = a.size(); if (b.size() != sz) diff --git a/common/imglib.cc b/common/imglib.cc index 3c8b5225..95aa236a 100644 --- a/common/imglib.cc +++ b/common/imglib.cc @@ -3,6 +3,7 @@ #include #include #include +#include /* ============================================================================ @@ -136,8 +137,8 @@ std::optional load_wal(const std::string_view &name, const fs::data &fi // the .wal is ignored. it's extraneous and well-formed wals // will all match up anyways. tex.meta.name = name; - tex.meta.width = mt.width; - tex.meta.height = mt.height; + tex.meta.width = tex.width = mt.width; + tex.meta.height = tex.height = mt.height; tex.meta.contents = {mt.contents}; tex.meta.flags = {mt.flags}; tex.meta.value = mt.value; @@ -182,17 +183,16 @@ std::optional load_mip(const std::string_view &name, const fs::data &fi // the mip is ignored. it's extraneous and well-formed mips // will all match up anyways. tex.meta.name = name; - tex.meta.width = header.width; - tex.meta.height = header.height; + tex.meta.width = tex.width = header.width; + tex.meta.height = tex.height = header.height; if (!meta_only) { - // convert the data into RGBA. + // miptex only has meta if (header.offsets[0] <= 0) { - // this should never happen under normal circumstances - logging::funcprint("attempted to load external mip for {}\n", name); - return std::nullopt; + return tex; } - + + // convert the data into RGBA. // sanity check if (header.offsets[0] + (header.width * header.height) > file->size()) { logging::funcprint("mip offset0 overrun for {}\n", name); @@ -301,8 +301,8 @@ std::optional load_tga(const std::string_view &name, const fs::data &fi tex.meta.extension = ext::TGA; tex.meta.name = name; - tex.meta.width = columns; - tex.meta.height = rows; + tex.meta.width = tex.width = columns; + tex.meta.height = tex.height = rows; if (!meta_only) { tex.pixels.resize(numPixels); @@ -450,4 +450,133 @@ std::tuple, fs::resolve_result, fs::data> load_textu return {std::nullopt, {}, {}}; } + +/* + JSON meta format, meant to supplant .wal's metadata for external texture use. + All of the values are optional. + { + // valid instances of "contents"; either: + // - a case-insensitive string containing the textual representation + // of the content type + // - a number + // - an array of the two above, which will be OR'd together + "contents": [ "SOLID", 8 ], + "contents": 24, + "contents": "SOLID", + + // valid instances of "flags"; either: + // - a case-insensitive string containing the textual representation + // of the surface flags + // - a number + // - an array of the two above, which will be OR'd together + "flags": [ "SKY", 16 ], + "flags": 24, + "flags": "SKY", + + // "value" must be an integer + "value": 1234, + + // "animation" must be the name of the next texture in + // the chain. + "animation": "e1u1/comp2", + + // width/height are allowed to be supplied in order to + // have the editor treat the surface as if its dimensions + // are these rather than the ones pulled in from the image + // itself. they must be integers. + "width": 64, + "height": 64 + } +*/ +std::optional load_wal_json_meta(const std::string_view &name, const fs::data &file, const gamedef_t *game) +{ + try + { + auto json = json::parse(file->begin(), file->end()); + + texture_meta meta{}; + + if (json.contains("width") && json["width"].is_number_integer()) { + meta.width = json["width"].get(); + } + + if (json.contains("height") && json["height"].is_number_integer()) { + meta.height = json["height"].get(); + } + + if (json.contains("value") && json["value"].is_number_integer()) { + meta.value = json["value"].get(); + } + + if (json.contains("contents")) { + auto &contents = json["contents"]; + + if (contents.is_number_integer()) { + meta.contents.native = contents.get(); + } else if (contents.is_string()) { + meta.contents.native = game->contents_from_string(contents.get()); + } else if (contents.is_array()) { + for (auto &content : contents) { + if (content.is_number_integer()) { + meta.contents.native |= content.get(); + } else if (content.is_string()) { + meta.contents.native |= game->contents_from_string(content.get()); + } + } + } + } + + if (json.contains("flags")) { + auto &flags = json["flags"]; + + if (flags.is_number_integer()) { + meta.flags.native = flags.get(); + } else if (flags.is_string()) { + meta.flags.native = game->surfflags_from_string(flags.get()); + } else if (flags.is_array()) { + for (auto &flag : flags) { + if (flag.is_number_integer()) { + meta.flags.native |= flag.get(); + } else if (flag.is_string()) { + meta.flags.native |= game->surfflags_from_string(flag.get()); + } + } + } + } + + if (json.contains("animation") && json["animation"].is_string()) { + meta.animation = json["animation"].get(); + } + + return meta; + } + catch (json::exception e) + { + logging::funcprint("{}, invalid JSON: {}\n", name, e.what()); + return std::nullopt; + } +} + +std::tuple, fs::resolve_result, fs::data> load_texture_meta(const std::string_view &name, const gamedef_t *game, const settings::common_settings &options) +{ + fs::path prefix; + + if (game->id == GAME_QUAKE_II) { + prefix = "textures"; + } + + for (auto &ext : img::meta_extension_list) { + fs::path p = (prefix / name) += ext.suffix; + + if (auto pos = fs::where(p, options.filepriority.value() == settings::search_priority_t::LOOSE)) { + if (auto data = fs::load(pos)) { + if (auto texture = ext.loader(name.data(), data, game)) { + return {texture, pos, data}; + } + } + } + } + + return {std::nullopt, {}, {}}; +} } // namespace img diff --git a/include/common/bspfile.hh b/include/common/bspfile.hh index 58454695..909b9c26 100644 --- a/include/common/bspfile.hh +++ b/include/common/bspfile.hh @@ -257,6 +257,7 @@ struct gamedef_t virtual bool surf_is_lightmapped(const surfflags_t &flags) const = 0; virtual bool surf_is_subdivided(const surfflags_t &flags) const = 0; virtual bool surfflags_are_valid(const surfflags_t &flags) const = 0; + virtual int32_t surfflags_from_string(const std::string_view &str) const = 0; // FIXME: fix so that we don't have to pass a name here virtual bool texinfo_is_hintskip(const surfflags_t &flags, const std::string &name) const = 0; virtual contentflags_t cluster_contents(const contentflags_t &contents0, const contentflags_t &contents1) const = 0; @@ -283,6 +284,7 @@ struct gamedef_t virtual bool contents_are_sky(const contentflags_t &contents) const = 0; virtual bool contents_are_liquid(const contentflags_t &contents) const = 0; virtual bool contents_are_valid(const contentflags_t &contents, bool strict = true) const = 0; + virtual int32_t contents_from_string(const std::string_view &str) const = 0; virtual bool portal_can_see_through(const contentflags_t &contents0, const contentflags_t &contents1, bool transwater, bool transsky) const = 0; virtual bool contents_seals_map(const contentflags_t &contents) const = 0; virtual contentflags_t contents_remap_for_export(const contentflags_t &contents) const = 0; diff --git a/include/common/cmdlib.hh b/include/common/cmdlib.hh index 47d681fb..ce2aaf9e 100644 --- a/include/common/cmdlib.hh +++ b/include/common/cmdlib.hh @@ -63,7 +63,7 @@ inline int32_t Q_strcasecmp(const std::string_view &a, const std::string_view &b (a.data(), b.data()); } -bool string_iequals(const std::string &a, const std::string &b); // mxd +bool string_iequals(const std::string_view &a, const std::string_view &b); // mxd struct case_insensitive_hash { diff --git a/include/common/imglib.hh b/include/common/imglib.hh index 26812845..2b05be03 100644 --- a/include/common/imglib.hh +++ b/include/common/imglib.hh @@ -42,25 +42,35 @@ void init_palette(const gamedef_t *game); struct texture_meta { std::string name; - uint32_t width, height; + uint32_t width = 0, height = 0; - ext extension; - - // This member is only set before insertion into the table - // and not calculated by individual load functions. - qvec3b averageColor; + // extension that we pulled the pixels in from. + std::optional extension; // Q2/WAL only - surfflags_t flags; - contentflags_t contents; - int32_t value; + surfflags_t flags{}; + contentflags_t contents{}; + int32_t value = 0; std::string animation; }; struct texture { texture_meta meta{}; + + // in the case of replacement textures, these may not + // the width/height of the metadata. + uint32_t width = 0, height = 0; + std::vector pixels; + + // the scale required to map a pixel from the + // meta data onto the real size (16x16 onto 32x32 -> 2) + float width_scale = 1, height_scale = 1; + + // This member is only set before insertion into the table + // and not calculated by individual load functions. + qvec3b averageColor { 0 }; }; extern std::unordered_map textures; @@ -88,4 +98,31 @@ constexpr struct { const char *suffix; ext id; decltype(load_wal) *loader; } ext // Attempt to load a texture from the specified name. std::tuple, fs::resolve_result, fs::data> load_texture(const std::string_view &name, bool meta_only, const gamedef_t *game, const settings::common_settings &options); + +enum class meta_ext +{ + WAL, + WAL_JSON +}; + +// Load wal +inline std::optional load_wal_meta(const std::string_view &name, const fs::data &file, const gamedef_t *game) +{ + if (auto tex = load_wal(name, file, true, game)) { + return tex->meta; + } + + return std::nullopt; +} + +std::optional load_wal_json_meta(const std::string_view &name, const fs::data &file, const gamedef_t *game); + +// list of supported meta extensions and their loaders +constexpr struct { const char *suffix; meta_ext id; decltype(load_wal_meta) *loader; } meta_extension_list[] = { + { ".wal", meta_ext::WAL, load_wal_meta }, + { ".wal_json", meta_ext::WAL_JSON, load_wal_json_meta } +}; + +// Attempt to load a texture meta from the specified name. +std::tuple, fs::resolve_result, fs::data> load_texture_meta(const std::string_view &name, const gamedef_t *game, const settings::common_settings &options); }; // namespace img diff --git a/include/common/json.hh b/include/common/json.hh index 3007ff03..d8d8ffa2 100644 --- a/include/common/json.hh +++ b/include/common/json.hh @@ -39,7 +39,6 @@ void to_json(json &j, const qvec &p) template void from_json(const json &j, qvec &p) { - for (size_t i = 0; i < N; i++) { p[i] = j[i].get(); } diff --git a/light/bounce.cc b/light/bounce.cc index 01869eb5..3fc0cb63 100644 --- a/light/bounce.cc +++ b/light/bounce.cc @@ -79,7 +79,7 @@ qvec3b Face_LookupTextureColor(const mbsp_t *bsp, const mface_t *face) auto it = img::find(Face_TextureName(bsp, face)); if (it) { - return it->meta.averageColor; + return it->averageColor; } return {127}; diff --git a/light/light.cc b/light/light.cc index f16529ba..fb8c9bc8 100644 --- a/light/light.cc +++ b/light/light.cc @@ -1013,43 +1013,63 @@ static inline void WriteNormals(const mbsp_t &bsp, bspdata_t &bspdata) } /* -============================================================================== -Load (Quake 2) / Convert (Quake, Hexen 2) textures from paletted to RGBA (mxd) -============================================================================== +// Add empty to keep texture index in case of load problems... +auto &tex = img::textures.emplace(miptex.name, img::texture{}).first->second; + +// try to load it externally first +auto [texture, _0, _1] = img::load_texture(miptex.name, false, bsp->loadversion->game, options); + +if (texture) { + tex = std::move(texture.value()); +} else { + if (miptex.data.size() <= sizeof(dmiptex_t)) { + logging::funcprint("WARNING: can't find texture {}\n", miptex.name); + continue; + } + + auto loaded_tex = img::load_mip(miptex.name, miptex.data, false, bsp->loadversion->game); + + if (!loaded_tex) { + logging::funcprint("WARNING: Texture {} is invalid\n", miptex.name); + continue; + } + + tex = std::move(loaded_tex.value()); +} + +tex.meta.averageColor = img::calculate_average(tex.pixels); */ -static void AddTextureName(const char *textureName, const mbsp_t *bsp) + +// Load the specified texture from the BSP +static void AddTextureName(const std::string_view &textureName, const mbsp_t *bsp) { if (img::find(textureName)) { return; } + // always add entry auto &tex = img::textures.emplace(textureName, img::texture{}).first->second; - // find wal first, since we'll use it for metadata - auto wal = fs::load("textures" / fs::path(textureName) += ".wal"); + // find texture & meta + auto [ texture, _0, _1 ] = img::load_texture(textureName, false, bsp->loadversion->game, options); - if (!wal) { - logging::funcprint("WARNING: can't find .wal for {}\n", textureName); + if (!texture) { + logging::funcprint("WARNING: can't find pixel data for {}\n", textureName); } else { - auto walTex = img::load_wal(textureName, wal, false, bsp->loadversion->game); - - if (walTex) { - tex = std::move(*walTex); - } + tex = std::move(texture.value()); } - // now check for replacements - auto [replacement_tex, _0, _1] = img::load_texture(textureName, false, bsp->loadversion->game, options); - - // FIXME: I think this is fundamentally wrong; we need the - // original texture's size for texcoords - if (replacement_tex) { - tex.meta.width = replacement_tex->meta.width; - tex.meta.height = replacement_tex->meta.height; - tex.pixels = std::move(replacement_tex->pixels); + auto [ texture_meta, __0, __1 ] = img::load_texture_meta(textureName, bsp->loadversion->game, options); + + if (!texture_meta) { + logging::funcprint("WARNING: can't find meta data for {}\n", textureName); + } else { + tex.meta = std::move(texture_meta.value()); } - tex.meta.averageColor = img::calculate_average(tex.pixels); + tex.averageColor = img::calculate_average(tex.pixels); + tex.width_scale = (float) tex.width / (float) tex.meta.width; + tex.height_scale = (float) tex.height / (float) tex.meta.height; } // Load all of the referenced textures from the BSP texinfos into @@ -1088,31 +1108,31 @@ static void ConvertTextures(const mbsp_t *bsp) continue; } - // Add empty to keep texture index in case of load problems... + // always add entry auto &tex = img::textures.emplace(miptex.name, img::texture{}).first->second; - // try to load it externally first - auto [texture, _0, _1] = img::load_texture(miptex.name, false, bsp->loadversion->game, options); - - if (texture) { - tex = std::move(texture.value()); - } else { - if (miptex.data.size() <= sizeof(dmiptex_t)) { - logging::funcprint("WARNING: can't find texture {}\n", miptex.name); - continue; + // if the miptex entry isn't a dummy, use it as our base + if (miptex.data.size() >= sizeof(dmiptex_t)) { + if (auto loaded_tex = img::load_mip(miptex.name, miptex.data, false, bsp->loadversion->game)) { + tex = std::move(loaded_tex.value()); } - - auto loaded_tex = img::load_mip(miptex.name, miptex.data, false, bsp->loadversion->game); - - if (!loaded_tex) { - logging::funcprint("WARNING: Texture {} is invalid\n", miptex.name); - continue; - } - - tex = std::move(loaded_tex.value()); } - tex.meta.averageColor = img::calculate_average(tex.pixels); + // find replacement texture + if (auto [ texture, _0, _1 ] = img::load_texture(miptex.name, false, bsp->loadversion->game, options); texture) { + tex.width = texture->width; + tex.height = texture->height; + tex.pixels = std::move(texture->pixels); + } + + if (!tex.pixels.size() || !tex.width || !tex.meta.width) { + logging::funcprint("WARNING: invalid size data for {}\n", miptex.name); + continue; + } + + tex.averageColor = img::calculate_average(tex.pixels); + tex.width_scale = (float) tex.width / (float) tex.meta.width; + tex.height_scale = (float) tex.height / (float) tex.meta.height; } } diff --git a/light/trace.cc b/light/trace.cc index 677e0c47..0b44f281 100644 --- a/light/trace.cc +++ b/light/trace.cc @@ -78,14 +78,14 @@ uint32_t clamp_texcoord(vec_t in, uint32_t width) qvec4b SampleTexture(const mface_t *face, const mtexinfo_t *tex, const img::texture *texture, const mbsp_t *bsp, const qvec3d &point) { - if (texture == nullptr || !texture->meta.width) { + if (texture == nullptr || !texture->width) { return {}; } qvec2d texcoord = WorldToTexCoord(point, tex); - const uint32_t x = clamp_texcoord(texcoord[0], texture->meta.width); - const uint32_t y = clamp_texcoord(texcoord[1], texture->meta.height); + const uint32_t x = clamp_texcoord(texcoord[0], texture->width); + const uint32_t y = clamp_texcoord(texcoord[1], texture->width); - return texture->pixels[(texture->meta.width * y) + x]; + return texture->pixels[(texture->width * (y * texture->width_scale)) + (x * texture->height_scale)]; } diff --git a/qbsp/map.cc b/qbsp/map.cc index 39222a9e..2f9093a4 100644 --- a/qbsp/map.cc +++ b/qbsp/map.cc @@ -52,9 +52,28 @@ const std::optional &mapdata_t::load_image_meta(const std::st return it->second; } - auto [texture, _0, _1] = img::load_texture(name, true, options.target_game, options); + // try a meta-only texture first; this is all we really need anyways + if (auto [texture_meta, _0, _1] = img::load_texture_meta(name, options.target_game, options); texture_meta) { + // slight special case: if the meta has no width/height defined, + // pull it from the real texture. + if (!texture_meta->width || !texture_meta->height) { + auto [texture, _0, _1] = img::load_texture(name, true, options.target_game, options); + + if (texture) { + texture_meta->width = texture->meta.width; + texture_meta->height = texture->meta.height; + } + } - if (texture) { + if (!texture_meta->width || !texture_meta->height) { + logging::print("WARNING: texture {} has empty width/height \n", name); + } + + return meta_cache.emplace(name, texture_meta).first->second; + } + + // couldn't find a meta texture, so pull it from the pixel image + if (auto [texture, _0, _1] = img::load_texture(name, true, options.target_game, options); texture) { return meta_cache.emplace(name, texture->meta).first->second; }