move generic image loading routine to `img`

move light-specific "load textures from BSP" routine to light
fix a couple bugs with external wad textures (there should always be at least a 40-byte miptex in there)
light can now load external textures
move -paths to common settings
fix bug with missing texture not filling miptex name
This commit is contained in:
Jonathan 2022-06-28 01:37:12 -04:00
parent c23b7d2ec9
commit 44c50717c3
10 changed files with 210 additions and 208 deletions

View File

@ -69,7 +69,7 @@ static void ExportWad(std::ofstream &wadfile, mbsp_t *bsp)
/* Count up the valid lumps */
numvalid = 0;
for (auto &texture : texdata.textures) {
if (!texture.data.empty()) {
if (texture.data.size() > sizeof(dmiptex_t)) {
numvalid++;
}
}
@ -85,7 +85,7 @@ static void ExportWad(std::ofstream &wadfile, mbsp_t *bsp)
/* Miptex data will follow the lump headers */
filepos = sizeof(header) + numvalid * sizeof(lump);
for (auto &miptex : texdata.textures) {
if (miptex.data.empty())
if (miptex.data.size() <= sizeof(dmiptex_t))
continue;
lump.filepos = filepos;
@ -99,7 +99,7 @@ static void ExportWad(std::ofstream &wadfile, mbsp_t *bsp)
wadfile <= lump;
}
for (auto &miptex : texdata.textures) {
if (!miptex.data.empty()) {
if (miptex.data.size() > sizeof(dmiptex_t)) {
miptex.stream_write(wadfile);
}
}

View File

@ -119,7 +119,7 @@ struct q2_miptex_t
auto stream_data() { return std::tie(name, width, height, offsets, animname, flags, contents, value); }
};
std::optional<texture> load_wal(const std::string &name, const fs::data &file, bool meta_only)
std::optional<texture> load_wal(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game)
{
memstream stream(file->data(), file->size(), std::ios_base::in | std::ios_base::binary);
stream >> endianness<std::endian::little>;
@ -159,7 +159,7 @@ Quake/Half Life MIP
============================================================================
*/
std::optional<texture> load_mip(const std::string &name, const fs::data &file, bool meta_only, const gamedef_t *game)
std::optional<texture> load_mip(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game)
{
memstream stream(file->data(), file->size(), std::ios_base::in | std::ios_base::binary);
stream >> endianness<std::endian::little>;
@ -273,7 +273,7 @@ struct targa_t
LoadTGA
=============
*/
std::optional<texture> load_tga(const std::string &name, const fs::data &file, bool meta_only)
std::optional<texture> load_tga(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game)
{
memstream stream(file->data(), file->size(), std::ios_base::in | std::ios_base::binary);
stream >> endianness<std::endian::little>;
@ -401,9 +401,9 @@ breakOut:;
// texture cache
std::unordered_map<std::string, texture, case_insensitive_hash, case_insensitive_equal> textures;
const texture *find(const std::string &str)
const texture *find(const std::string_view &str)
{
auto it = textures.find(str);
auto it = textures.find(str.data());
if (it == textures.end()) {
return nullptr;
@ -428,128 +428,40 @@ qvec3b calculate_average(const std::vector<qvec4b> &pixels)
return avg /= n;
}
/*
==============================================================================
Load (Quake 2) / Convert (Quake, Hexen 2) textures from paletted to RGBA (mxd)
==============================================================================
*/
static void AddTextureName(const char *textureName)
std::tuple<std::optional<img::texture>, fs::resolve_result, fs::data> load_texture(const std::string_view &name, bool meta_only, const gamedef_t *game, const settings::common_settings &options)
{
if (textures.find(textureName) != textures.end()) {
return;
fs::path prefix;
if (game->id == GAME_QUAKE_II) {
prefix = "textures";
}
auto &tex = textures.emplace(textureName, texture{}).first->second;
for (auto &ext : img::extension_list) {
fs::path p = (prefix / name) += ext.suffix;
static constexpr struct
{
const char *name;
decltype(load_wal) *loader;
} supportedExtensions[] = {{"tga", load_tga}};
if (auto pos = fs::where(p, options.filepriority.value() == settings::search_priority_t::LOOSE)) {
if (auto data = fs::load(pos)) {
std::optional<img::texture> texture;
// find wal first, since we'll use it for metadata
auto wal = fs::load("textures" / fs::path(textureName) += ".wal");
if (!wal) {
logging::funcprint("WARNING: can't find .wal for {}\n", textureName);
} else {
auto walTex = load_wal(textureName, wal, false);
if (walTex) {
tex = std::move(*walTex);
}
}
// now check for replacements
for (auto &ext : supportedExtensions) {
auto replacement = fs::load(("textures" / fs::path(textureName) += ".") += ext.name);
if (!replacement) {
continue;
}
auto replacementTex = ext.loader(textureName, replacement, false);
if (replacementTex) {
tex.meta.width = replacementTex->meta.width;
tex.meta.height = replacementTex->meta.height;
tex.pixels = std::move(replacementTex->pixels);
switch (ext.id) {
case img::ext::TGA:
texture = img::load_tga(name.data(), data, meta_only, game);
break;
case img::ext::WAL:
texture = img::load_wal(name.data(), data, meta_only, game);
break;
case img::ext::MIP:
texture = img::load_mip(name.data(), data, meta_only, game);
break;
}
}
tex.meta.averageColor = calculate_average(tex.pixels);
}
// Load all of the referenced textures from the BSP texinfos into
// the texture cache.
static void LoadTextures(const mbsp_t *bsp)
{
// gather all loadable textures...
for (auto &texinfo : bsp->texinfo) {
AddTextureName(texinfo.texture.data());
}
// gather textures used by _project_texture.
// FIXME: I'm sure we can resolve this so we don't parse entdata twice.
auto entdicts = EntData_Parse(bsp->dentdata);
for (auto &entdict : entdicts) {
if (entdict.get("classname").find("light") == 0) {
const auto &tex = entdict.get("_project_texture");
if (!tex.empty()) {
AddTextureName(tex.c_str());
if (texture) {
return {texture, pos, data};
}
}
}
}
// Load all of the paletted textures from the BSP into
// the texture cache.
// TODO: doesn't handle external wads...
static void ConvertTextures(const mbsp_t *bsp)
{
if (!bsp->dtex.textures.size()) {
return;
}
for (auto &miptex : bsp->dtex.textures) {
if (textures.find(miptex.name) != textures.end()) {
logging::funcprint("WARNING: Texture {} duplicated\n", miptex.name);
continue;
}
// Add empty to keep texture index in case of load problems...
auto &tex = textures.emplace(miptex.name, texture{}).first->second;
// FIXME: fs::load
if (miptex.data.empty()) {
logging::funcprint("WARNING: Texture {} is external\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 = calculate_average(tex.pixels);
}
}
void load_textures(const mbsp_t *bsp)
{
logging::print("--- {} ---\n", __func__);
if (bsp->loadversion->game->id == GAME_QUAKE_II) {
LoadTextures(bsp);
} else if (bsp->dtex.textures.size() > 0) {
ConvertTextures(bsp);
} else {
logging::print("WARNING: failed to load or convert textures.\n");
}
return {std::nullopt, {}, {}};
}
} // namespace img

View File

@ -329,9 +329,11 @@ struct dmiptexlump_t
next_offset = offsets[i + 1];
}
if (next_offset > offset) {
tex.stream_read(stream, next_offset - offset);
}
}
}
void stream_write(std::ostream &stream) const
{

View File

@ -34,13 +34,6 @@ enum class ext
MIP
};
constexpr struct { const char *suffix; ext id; } extension_list[] = {
{ ".tga", ext::TGA },
{ ".wal", ext::WAL },
{ ".mip", ext::MIP },
{ "", ext::MIP }
};
extern std::vector<qvec3b> palette;
// Palette
@ -74,17 +67,25 @@ extern std::unordered_map<std::string, texture, case_insensitive_hash, case_inse
qvec3b calculate_average(const std::vector<qvec4b> &pixels);
const texture *find(const std::string &str);
const texture *find(const std::string_view &str);
// Load wal
std::optional<texture> load_wal(const std::string &name, const fs::data &file, bool meta_only);
std::optional<texture> load_wal(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game);
// Load TGA
std::optional<texture> load_tga(const std::string &name, const fs::data &file, bool meta_only);
std::optional<texture> load_tga(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game);
// Load Quake/Half Life mip (raw data)
std::optional<texture> load_mip(const std::string &name, const fs::data &file, bool meta_only, const gamedef_t *game);
std::optional<texture> load_mip(const std::string_view &name, const fs::data &file, bool meta_only, const gamedef_t *game);
// Pull in texture data from the BSP into the textures map
void load_textures(const mbsp_t *bsp);
// list of supported extensions and their loaders
constexpr struct { const char *suffix; ext id; decltype(load_wal) *loader; } extension_list[] = {
{ ".tga", ext::TGA, load_tga },
{ ".wal", ext::WAL, load_wal },
{ ".mip", ext::MIP, load_mip },
{ "", ext::MIP, load_mip }
};
// Attempt to load a texture from the specified name.
std::tuple<std::optional<texture>, fs::resolve_result, fs::data> load_texture(const std::string_view &name, bool meta_only, const gamedef_t *game, const settings::common_settings &options);
}; // namespace img

View File

@ -848,6 +848,7 @@ public:
setting_redirect quiet{this, {"quiet", "noverbose"}, {&nopercent, &nostat, &noprogress}, &logging_group, "suppress non-important messages (equivalent to -nopercent -nostat -noprogress)"};
setting_string basedir{this, "basedir", "", "dir_name", &game_group, "override the default game base directory"};
setting_enum<search_priority_t> filepriority{this, "filepriority", search_priority_t::LOOSE, { { "loose", search_priority_t::LOOSE }, { "archive", search_priority_t::ARCHIVE } }, &game_group, "which types of archives (folders/loose files or packed archives) are higher priority and chosen first for path searching" };
setting_set paths{this, "path", "\"/path/to/folder\" <multiple allowed>", &game_group, "additional paths or archives to add to the search path, mostly for loose files"};
setting_bool q2rtx{this, "q2rtx", false, &game_group, "adjust settings to best support Q2RTX"};
virtual void setParameters(int argc, const char **argv);

View File

@ -181,8 +181,6 @@ struct mapdata_t
std::unordered_map<std::string, std::optional<img::texture_meta>> meta_cache;
// load or fetch image meta associated with the specified name
const std::optional<img::texture_meta> &load_image_meta(const std::string_view &name);
// load image data for the specified name
std::tuple<std::optional<img::texture>, fs::resolve_result, fs::data> load_image_data(const std::string_view &name, bool meta_only);
// whether we had attempted loading texture stuff
bool textures_loaded = false;

View File

@ -222,7 +222,6 @@ public:
this, "expand", false, &common_format_group, "write hull 1 expanded brushes to expanded.map for debugging"};
setting_wadpathset wadpaths{this, {"wadpath", "xwadpath"}, &map_development_group,
"add a path to the wad search paths; wads found in xwadpath's will not be embedded, otherwise they will be embedded (if not -notex)"};
setting_set paths{this, "path", "\"/path/to/folder\" <multiple allowed>", &map_development_group, "additional paths or archives to add to the search path, mostly for loose files"};
setting_bool notriggermodels{this, "notriggermodels", false, &common_format_group, "for supported game code only: triggers will not write a model\nout, and will instead just write out their mins/maxs."};
setting_set aliasdefs{this, "aliasdef", "\"path/to/file.def\" <multiple allowed>", &map_development_group, "path to an alias definition file, which can transform entities in the .map into other entities."};

View File

@ -863,6 +863,127 @@ static inline void WriteNormals(const mbsp_t &bsp, bspdata_t &bspdata)
}
}
/*
==============================================================================
Load (Quake 2) / Convert (Quake, Hexen 2) textures from paletted to RGBA (mxd)
==============================================================================
*/
static void AddTextureName(const char *textureName, const mbsp_t *bsp)
{
if (img::find(textureName)) {
return;
}
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");
if (!wal) {
logging::funcprint("WARNING: can't find .wal for {}\n", textureName);
} else {
auto walTex = img::load_wal(textureName, wal, false, bsp->loadversion->game);
if (walTex) {
tex = std::move(*walTex);
}
}
// 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);
}
tex.meta.averageColor = img::calculate_average(tex.pixels);
}
// Load all of the referenced textures from the BSP texinfos into
// the texture cache.
static void LoadTextures(const mbsp_t *bsp)
{
// gather all loadable textures...
for (auto &texinfo : bsp->texinfo) {
AddTextureName(texinfo.texture.data(), bsp);
}
// gather textures used by _project_texture.
// FIXME: I'm sure we can resolve this so we don't parse entdata twice.
auto entdicts = EntData_Parse(bsp->dentdata);
for (auto &entdict : entdicts) {
if (entdict.get("classname").find("light") == 0) {
const auto &tex = entdict.get("_project_texture");
if (!tex.empty()) {
AddTextureName(tex.c_str(), bsp);
}
}
}
}
// Load all of the paletted textures from the BSP into
// the texture cache.
static void ConvertTextures(const mbsp_t *bsp)
{
if (!bsp->dtex.textures.size()) {
return;
}
for (auto &miptex : bsp->dtex.textures) {
if (img::find(miptex.name)) {
logging::funcprint("WARNING: Texture {} duplicated\n", miptex.name);
continue;
}
// 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);
}
}
void load_textures(const mbsp_t *bsp)
{
logging::print("--- {} ---\n", __func__);
for (auto &path : options.paths.values()) {
fs::addArchive(path, true);
}
if (bsp->loadversion->game->id == GAME_QUAKE_II) {
LoadTextures(bsp);
} else if (bsp->dtex.textures.size() > 0) {
ConvertTextures(bsp);
} else {
logging::print("WARNING: failed to load or convert textures.\n");
}
}
/*
* ==================
* main
@ -912,7 +1033,7 @@ int light_main(int argc, const char **argv)
options.bouncescale.setValue(1.5f);
}
img::load_textures(&bsp);
load_textures(&bsp);
LoadExtendedTexinfoFlags(source, &bsp);
LoadEntities(options, &bsp);

View File

@ -43,43 +43,6 @@
mapdata_t map;
std::tuple<std::optional<img::texture>, fs::resolve_result, fs::data> mapdata_t::load_image_data(const std::string_view &name, bool meta_only)
{
fs::path prefix;
if (options.target_game->id == GAME_QUAKE_II) {
prefix = "textures";
}
for (auto &ext : img::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)) {
std::optional<img::texture> texture;
switch (ext.id) {
case img::ext::TGA:
texture = img::load_tga(name.data(), data, meta_only);
break;
case img::ext::WAL:
texture = img::load_wal(name.data(), data, meta_only);
break;
case img::ext::MIP:
texture = img::load_mip(name.data(), data, meta_only, options.target_game);
break;
}
if (texture) {
return {texture, pos, data};
}
}
}
}
return {std::nullopt, {}, {}};
}
const std::optional<img::texture_meta> &mapdata_t::load_image_meta(const std::string_view &name)
{
static std::optional<img::texture_meta> nullmeta = std::nullopt;
@ -89,7 +52,7 @@ const std::optional<img::texture_meta> &mapdata_t::load_image_meta(const std::st
return it->second;
}
auto [texture, _0, _1] = load_image_data(name, true);
auto [texture, _0, _1] = img::load_texture(name, true, options.target_game, options);
if (texture) {
return meta_cache.emplace(name, texture->meta).first->second;

View File

@ -1131,7 +1131,12 @@ static void CreateHulls(void)
static void LoadTextureData()
{
for (size_t i = 0; i < map.miptex.size(); i++) {
auto [tex, pos, file] = map.load_image_data(map.miptex[i].name, true);
// always fill the name even if we can't find it
auto &miptex = map.bsp.dtex.textures[i];
miptex.name = map.miptex[i].name;
{
auto [tex, pos, file] = img::load_texture(map.miptex[i].name, true, options.target_game, options);
if (!tex) {
if (pos.archive) {
@ -1139,18 +1144,19 @@ static void LoadTextureData()
} else {
logging::print("WARNING: unable to find texture {}\n", map.miptex[i].name);
}
continue;
}
auto &miptex = map.bsp.dtex.textures[i];
miptex.name = map.miptex[i].name;
} else {
miptex.width = tex->meta.width;
miptex.height = tex->meta.height;
// only mips can be embedded directly
if (!pos.archive->external && tex->meta.extension == img::ext::MIP) {
miptex.data = std::move(file.value());
} else {
continue;
}
}
}
// fall back to when we can't load the image.
// construct fake data that solely contains the header.
miptex.data.resize(sizeof(dmiptex_t));
@ -1169,7 +1175,6 @@ static void LoadTextureData()
memstream stream(miptex.data.data(), miptex.data.size(), std::ios_base::out | std::ios_base::binary);
stream <= header;
}
}
}
static void AddAnimationFrames()