2
0
mirror of https://github.com/Laupetin/OpenAssetTools.git synced 2026-06-26 10:58:04 +00:00

feat: xmodel preview in ModMan (#835)

* chore: upgrade webwindowed for dynamic assets

* chore: make enums in ModMan lowercase

* chore: add missing platform wiiu in ModMan

* fix: register asset handler on all windows

* chore: properly localize game and platform

* chore: render example cube as xmodel preview

* chore: allow origin * in debug

* feat: show preview of xmodels with ModMan

* feat: show images in xmodel preview

* feat: auto load search paths in ModMan

* chore: load objcontainer of loaded zones in ModMan

* chore: add iw4x specific recognized zone dirs

* chore: show when models are loading

* fix: make sure webwindowed handles window and app destruction in correct order

* chore: track and properly free threejs resources

* chore: add skybox for 3d preview

* chore: add small border radius to preview

* fix: linting

* fix: linux compilation

* chore: update package lock
This commit is contained in:
Jan
2026-06-18 18:52:52 +02:00
committed by GitHub
parent 4fd164ee33
commit 85aa7417c4
53 changed files with 2692 additions and 964 deletions
+117 -7
View File
@@ -713,6 +713,13 @@
"node": ">=20.19.0"
}
},
"node_modules/@dimforge/rapier3d-compat": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -1778,6 +1785,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"dev": true,
"license": "MIT"
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
@@ -1863,6 +1877,28 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/three": {
"version": "0.184.1",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz",
"integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
"@types/stats.js": "*",
"@types/webxr": ">=0.5.17",
"fflate": "~0.8.2",
"meshoptimizer": "~1.1.1"
}
},
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
@@ -1870,6 +1906,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@types/webxr": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz",
@@ -2677,10 +2726,48 @@
}
}
},
"node_modules/@vueuse/core": {
"version": "14.3.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz",
"integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "14.3.0",
"@vueuse/shared": "14.3.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/metadata": {
"version": "14.3.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz",
"integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "14.3.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz",
"integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@webwindowed/vite-plugin-cpp-header": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@webwindowed/vite-plugin-cpp-header/-/vite-plugin-cpp-header-1.0.0.tgz",
"integrity": "sha512-0eALUR+M6rkq45FXslE36/UJWZ2Xy9Gt+JH1GNINnlp9QH+JP7wBqp228+bUHuSxgGIiSXsSQi3C0CA+AbmxRg==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@webwindowed/vite-plugin-cpp-header/-/vite-plugin-cpp-header-1.1.0.tgz",
"integrity": "sha512-3vfsU4uAKZXkzoXKUUokXByygF25Vt9lsv6BmSDWwH6iYMbVZqJ9nX/7MIobGJw0YirH7FoOCjeDtSGQTE9y4Q==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -3688,6 +3775,13 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"dev": true,
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4601,6 +4695,13 @@
"node": ">= 8"
}
},
"node_modules/meshoptimizer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
"integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
"dev": true,
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -5679,6 +5780,12 @@
"url": "https://opencollective.com/synckit"
}
},
"node_modules/three": {
"version": "0.184.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
"integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
"license": "MIT"
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -5898,9 +6005,9 @@
"license": "MIT"
},
"node_modules/undici": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.27.0.tgz",
"integrity": "sha512-+t2Z/GwkZQDtu00813aP66ygViGtPHKhhoFZpQKpKrE+9jIgES+Zw+mFNaDWOVRKiuJjuqKHzD3B1sfGg8+ZOQ==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6884,8 +6991,10 @@
"dependencies": {
"@fontsource/inter": "5.2.8",
"@primeuix/themes": "2.0.3",
"@vueuse/core": "14.3.0",
"pinia": "3.0.4",
"primevue": "4.5.5",
"three": "0.184.0",
"vue": "3.5.35",
"vue-router": "5.1.0"
},
@@ -6893,13 +7002,14 @@
"@tsconfig/node24": "24.0.4",
"@types/jsdom": "28.0.3",
"@types/node": "25.9.2",
"@types/three": "0.184.1",
"@vitejs/plugin-vue": "6.0.7",
"@vitest/eslint-plugin": "1.6.19",
"@vue/eslint-config-prettier": "10.2.0",
"@vue/eslint-config-typescript": "14.8.0",
"@vue/test-utils": "2.4.11",
"@vue/tsconfig": "0.9.1",
"@webwindowed/vite-plugin-cpp-header": "1.0.0",
"@webwindowed/vite-plugin-cpp-header": "1.1.0",
"@webwindowed/web-api": "1.0.0",
"eslint": "10.4.1",
"eslint-plugin-vue": "10.9.2",
@@ -0,0 +1,107 @@
#include "DynamicAssetsImage.h"
#include "Context/ModManContext.h"
#include "Game/CommonAsset.h"
#include "Image/DdsWriter.h"
#include "Image/ImageToCommonConverter.h"
#include "Pool/XAssetInfo.h"
#include "SearchPath/SearchPaths.h"
#include "Utils/Logging/Log.h"
#include "Utils/StringUtils.h"
#include <filesystem>
#include <sstream>
#include <utility>
using namespace image;
namespace fs = std::filesystem;
namespace
{
bool FindImage(const std::string& zoneName, const std::string& assetName, XAssetInfoGeneric*& outAssetInfo, Zone*& outZone)
{
auto& context = ModManContext::Get().m_fast_file;
const auto loadedZones = context.GetLoadedZones();
for (const auto& loadedZone : loadedZones.Data())
{
const auto& zone = loadedZone->GetZone();
if (zone.m_name != zoneName)
continue;
const auto* assetTypeMapper = ICommonAssetTypeMapper::GetCommonAssetMapperByGame(zone.m_game_id);
const auto gameAssetType = assetTypeMapper->CommonToGameAssetType(CommonAssetType::IMAGE);
if (!gameAssetType)
continue;
outAssetInfo = zone.m_pools.GetAsset(*gameAssetType, assetName);
if (outAssetInfo)
{
outZone = &loadedZone->GetZone();
return true;
}
}
return false;
}
void ImageDds(const webwindowed::dynamic_asset_request& request, webwindowed::dynamic_asset_response& response)
{
const auto imageName = request.get_query("name");
const auto zoneName = request.get_query("zone");
if (!imageName.has_value() || imageName->empty() || !zoneName.has_value() || zoneName->empty())
{
con::error("Bad dds request (name={} zone={})", imageName.value_or(""), zoneName.value_or(""));
response.send_response(400);
return;
}
XAssetInfoGeneric* image;
Zone* zone;
if (!FindImage(*zoneName, *imageName, image, zone))
{
con::warn("Could not find image {} of zone {}", *imageName, *zoneName);
response.send_response(404);
return;
}
assert(image);
assert(zone);
const auto gameName = GameId_Names[std::to_underlying(zone->m_game_id)];
const auto converter = ToCommonConverter::GetForGame(zone->m_game_id);
if (!converter)
{
con::error("No image converter for game {}", gameName);
response.send_response(500);
return;
}
std::unique_ptr<Texture> texture;
{
const auto searchPaths = ModManContext::Get().m_fast_file.GetSearchPaths();
texture = converter->Convert(*image, searchPaths.Data());
if (!texture)
{
con::warn("Failed to convert image {} of zone {}", *imageName, *zoneName);
response.send_response(500);
return;
}
}
std::ostringstream ss;
DdsWriter output;
output.DumpImage(ss, texture.get());
const auto data = ss.str();
response.set_content_type("image/x-direct-draw-surface");
response.send_response(data.data(), data.size());
}
} // namespace
namespace image
{
void RegisterDynamicAssets(webwindowed::asset_handler_plugin& assetHandler)
{
assetHandler.add_dynamic_asset(webwindowed::dynamic_asset("image/dds", ImageDds));
}
} // namespace image
@@ -0,0 +1,8 @@
#pragma once
#include "Web/WebWindowedLib.h"
namespace image
{
void RegisterDynamicAssets(webwindowed::asset_handler_plugin& assetHandler);
}
@@ -0,0 +1,100 @@
#include "DynamicAssetsXModel.h"
#include "Context/ModManContext.h"
#include "Game/CommonAsset.h"
#include "Pool/XAssetInfo.h"
#include "Utils/Logging/Log.h"
#include "XModel/Gltf/GltfBinOutput.h"
#include "XModel/Gltf/GltfWriter.h"
#include "XModel/XModelToCommonConverter.h"
#include <sstream>
using namespace xmodel;
namespace
{
bool FindXModel(const std::string& zoneName, const std::string& assetName, XAssetInfoGeneric*& outAssetInfo, Zone*& outZone)
{
auto& context = ModManContext::Get().m_fast_file;
const auto loadedZones = context.GetLoadedZones();
for (const auto& loadedZone : loadedZones.Data())
{
const auto& zone = loadedZone->GetZone();
if (zone.m_name != zoneName)
continue;
const auto* assetTypeMapper = ICommonAssetTypeMapper::GetCommonAssetMapperByGame(zone.m_game_id);
const auto gameAssetType = assetTypeMapper->CommonToGameAssetType(CommonAssetType::XMODEL);
if (!gameAssetType)
continue;
outAssetInfo = zone.m_pools.GetAsset(*gameAssetType, assetName);
if (outAssetInfo)
{
outZone = &loadedZone->GetZone();
return true;
}
}
return false;
}
void XModelGlb(const webwindowed::dynamic_asset_request& request, webwindowed::dynamic_asset_response& response)
{
const auto modelName = request.get_query("name");
const auto zoneName = request.get_query("zone");
if (!modelName.has_value() || modelName->empty() || !zoneName.has_value() || zoneName->empty())
{
con::error("Bad glb request (name={} zone={})", modelName.value_or(""), zoneName.value_or(""));
response.send_response(400);
return;
}
XAssetInfoGeneric* model;
Zone* zone;
if (!FindXModel(*zoneName, *modelName, model, zone))
{
con::warn("Could not find xmodel {} of zone {}", *modelName, *zoneName);
response.send_response(404);
return;
}
assert(model);
assert(zone);
const auto gameName = GameId_Names[std::to_underlying(zone->m_game_id)];
const auto converter = ToCommonConverter::GetForGame(zone->m_game_id);
if (!converter)
{
con::error("No xmodel converter for game {}", gameName);
response.send_response(500);
return;
}
const auto maybeCommon = converter->Convert(*model, 0);
if (!maybeCommon)
{
con::warn("Failed to convert xmodel {} of zone {}", *modelName, *zoneName);
response.send_response(500);
return;
}
std::ostringstream ss;
const gltf::BinOutput output(ss);
const auto gltfWriter = gltf::Writer::CreateWriter(&output, gameName, *zoneName);
gltfWriter->Write(*maybeCommon);
const auto data = ss.str();
response.set_content_type("model/gltf-binary");
response.send_response(data.data(), data.size());
}
} // namespace
namespace xmodel
{
void RegisterDynamicAssets(webwindowed::asset_handler_plugin& assetHandler)
{
assetHandler.add_dynamic_asset(webwindowed::dynamic_asset("xmodel/glb", XModelGlb));
}
} // namespace xmodel
@@ -0,0 +1,8 @@
#pragma once
#include "Web/WebWindowedLib.h"
namespace xmodel
{
void RegisterDynamicAssets(webwindowed::asset_handler_plugin& assetHandler);
}
+153 -16
View File
@@ -1,5 +1,10 @@
#include "FastFileContext.h"
#include "Game/AutoSearchPaths.h"
#include "IObjLoader.h"
#include "SearchPath/IWD.h"
#include "SearchPath/SearchPathFilesystem.h"
#include "Utils/StringUtils.h"
#include "Web/Binds/ZoneBinds.h"
#include "Web/UiCommunication.h"
#include "ZoneLoading.h"
@@ -36,14 +41,82 @@ namespace
std::string m_zone_name;
double m_last_progress;
};
std::unique_ptr<ISearchPath> CreateSearchPath(const std::string& searchPathStr)
{
auto searchPath = std::make_unique<SearchPathFilesystem>(searchPathStr);
con::debug("Loaded search path \"{}\"", searchPathStr);
SearchPaths searchPaths;
bool hasIwds = false;
std::filesystem::directory_iterator iterator(searchPathStr);
const auto end = fs::end(iterator);
for (auto i = fs::begin(iterator); i != end; ++i)
{
if (!i->is_regular_file())
continue;
auto extension = i->path().extension().string();
utils::MakeStringLowerCase(extension);
if (extension == ".iwd")
{
std::string iwdPath = i->path().string();
auto iwd = iwd::LoadFromFile(iwdPath);
if (iwd)
{
if (!hasIwds)
{
searchPaths.CommitSearchPath(std::move(searchPath));
hasIwds = true;
}
searchPaths.CommitSearchPath(std::move(iwd));
con::debug("Loaded search path \"{}\"", iwdPath);
}
}
}
if (hasIwds)
return std::make_unique<SearchPaths>(std::move(searchPaths));
return searchPath;
}
} // namespace
LoadedZone::LoadedZone(std::unique_ptr<Zone> zone, std::string filePath)
: m_zone(std::move(zone)),
m_file_path(std::move(filePath))
ContextSearchPath::ContextSearchPath(std::unique_ptr<ISearchPath> searchPath)
: m_search_path(std::move(searchPath)),
m_ref_count(1)
{
}
LoadedZone::LoadedZone(std::unique_ptr<Zone> zone, std::string filePath, std::vector<std::string> searchPaths)
: m_zone(std::move(zone)),
m_file_path(std::move(filePath)),
m_search_paths(std::move(searchPaths))
{
}
Zone& LoadedZone::GetZone()
{
return *m_zone;
}
const Zone& LoadedZone::GetZone() const
{
return *m_zone;
}
const std::string& LoadedZone::GetFilePath() const
{
return m_file_path;
}
const std::vector<std::string>& LoadedZone::GetSearchPaths() const
{
return m_search_paths;
}
void FastFileContext::Destroy()
{
// Unload all zones
@@ -56,36 +129,100 @@ std::expected<LoadedZone*, std::string> FastFileContext::LoadFastFile(const std:
if (!zone)
return std::unexpected(std::move(zone.error()));
auto loadedZone = std::make_unique<LoadedZone>(std::move(*zone), path);
LoadedZone* result;
auto searchPathsForZone = AutoSearchPaths::GetForGame((*zone)->m_game_id)->GetSearchPathsForZonePath(path);
{
std::lock_guard lock(m_zone_lock);
result = m_loaded_zones.emplace_back(std::move(loadedZone)).get();
std::lock_guard lock(m_search_path_lock);
for (const auto& searchPathStr : searchPathsForZone)
{
const auto existingSearchPath = m_context_search_paths.find(searchPathStr);
if (existingSearchPath == m_context_search_paths.end())
{
auto searchPath = CreateSearchPath(searchPathStr);
m_search_paths.IncludeSearchPath(searchPath.get());
m_context_search_paths.emplace(searchPathStr, std::make_unique<ContextSearchPath>(std::move(searchPath)));
}
else
{
existingSearchPath->second->m_ref_count++;
}
}
}
ui::NotifyZoneLoaded(*result);
auto loadedZone = std::make_unique<LoadedZone>(std::move(*zone), path, std::move(searchPathsForZone));
return result;
LoadedZone* loadedZonePtr;
{
std::lock_guard lock(m_zone_lock);
loadedZonePtr = m_loaded_zones.emplace_back(std::move(loadedZone)).get();
}
{
std::shared_lock lock(m_search_path_lock);
IObjLoader::GetObjLoaderForGame(loadedZonePtr->GetZone().m_game_id)->LoadReferencedContainersForZone(m_search_paths, loadedZonePtr->GetZone());
}
ui::NotifyZoneLoaded(*loadedZonePtr);
return loadedZonePtr;
}
std::expected<void, std::string> FastFileContext::UnloadZone(const std::string& zoneName)
{
std::unique_ptr<LoadedZone> removedLoadedZone;
{
std::lock_guard lock(m_zone_lock);
const auto existingZone = std::ranges::find_if(m_loaded_zones,
[&zoneName](const std::unique_ptr<LoadedZone>& loadedZone)
{
return loadedZone->m_zone->m_name == zoneName;
return loadedZone->GetZone().m_name == zoneName;
});
if (existingZone != m_loaded_zones.end())
if (existingZone == m_loaded_zones.end())
return std::unexpected(std::format("No zone with name {} loaded", zoneName));
removedLoadedZone = std::move(*existingZone);
m_loaded_zones.erase(existingZone);
ui::NotifyZoneUnloaded(zoneName);
}
assert(removedLoadedZone);
{
std::shared_lock lock(m_search_path_lock);
IObjLoader::GetObjLoaderForGame(removedLoadedZone->GetZone().m_game_id)->UnloadContainersOfZone(removedLoadedZone->GetZone());
}
{
std::lock_guard lock(m_search_path_lock);
for (const auto& searchPathStr : removedLoadedZone->GetSearchPaths())
{
m_loaded_zones.erase(existingZone);
ui::NotifyZoneUnloaded(zoneName);
return {};
const auto existingSearchPath = m_context_search_paths.find(searchPathStr);
if (existingSearchPath != m_context_search_paths.end())
{
assert(existingSearchPath->second->m_ref_count > 0);
const auto newRefCount = --existingSearchPath->second->m_ref_count;
if (newRefCount == 0)
{
m_search_paths.RemoveSearchPath(existingSearchPath->second->m_search_path.get());
m_context_search_paths.erase(existingSearchPath);
con::debug("Unloaded search path \"{}\"", searchPathStr);
}
}
}
}
return std::unexpected(std::format("No zone with name {} loaded", zoneName));
return {};
}
ReadAccess<const std::vector<std::unique_ptr<LoadedZone>>> FastFileContext::GetLoadedZones()
{
return ReadAccess<const std::vector<std::unique_ptr<LoadedZone>>>(std::shared_lock(m_zone_lock), m_loaded_zones);
}
ReadAccess<ISearchPath> FastFileContext::GetSearchPaths()
{
return ReadAccess<ISearchPath>(std::shared_lock(m_search_path_lock), m_search_paths);
}
+48 -1
View File
@@ -1,19 +1,58 @@
#pragma once
#include "SearchPath/SearchPaths.h"
#include "Zone/Zone.h"
#include <expected>
#include <memory>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <vector>
class ContextSearchPath
{
public:
explicit ContextSearchPath(std::unique_ptr<ISearchPath> searchPath);
std::unique_ptr<ISearchPath> m_search_path;
unsigned m_ref_count;
};
class LoadedZone
{
public:
LoadedZone(std::unique_ptr<Zone> zone, std::string filePath, std::vector<std::string> searchPaths);
[[nodiscard]] Zone& GetZone();
[[nodiscard]] const Zone& GetZone() const;
[[nodiscard]] const std::string& GetFilePath() const;
[[nodiscard]] const std::vector<std::string>& GetSearchPaths() const;
private:
std::unique_ptr<Zone> m_zone;
std::string m_file_path;
std::vector<std::string> m_search_paths;
};
LoadedZone(std::unique_ptr<Zone> zone, std::string filePath);
template<class T> class ReadAccess
{
public:
ReadAccess(std::shared_lock<std::shared_mutex> lock, T& data)
: m_read_lock(std::move(lock)),
m_data(data)
{
}
[[nodiscard]] T& Data() const
{
return m_data;
}
private:
std::shared_lock<std::shared_mutex> m_read_lock;
T& m_data;
};
class FastFileContext
@@ -24,6 +63,14 @@ public:
std::expected<LoadedZone*, std::string> LoadFastFile(const std::string& path);
std::expected<void, std::string> UnloadZone(const std::string& zoneName);
ReadAccess<const std::vector<std::unique_ptr<LoadedZone>>> GetLoadedZones();
ReadAccess<ISearchPath> GetSearchPaths();
private:
std::vector<std::unique_ptr<LoadedZone>> m_loaded_zones;
std::shared_mutex m_zone_lock;
SearchPaths m_search_paths;
std::unordered_map<std::string, std::unique_ptr<ContextSearchPath>> m_context_search_paths;
std::shared_mutex m_search_path_lock;
};
+76 -76
View File
@@ -9,79 +9,79 @@
NLOHMANN_JSON_SERIALIZE_ENUM(CommonAssetType,
{
{CommonAssetType::PHYS_PRESET, "PHYS_PRESET" },
{CommonAssetType::XANIM, "XANIM" },
{CommonAssetType::XMODEL, "XMODEL" },
{CommonAssetType::MATERIAL, "MATERIAL" },
{CommonAssetType::TECHNIQUE_SET, "TECHNIQUE_SET" },
{CommonAssetType::IMAGE, "IMAGE" },
{CommonAssetType::SOUND, "SOUND" },
{CommonAssetType::SOUND_CURVE, "SOUND_CURVE" },
{CommonAssetType::LOADED_SOUND, "LOADED_SOUND" },
{CommonAssetType::CLIP_MAP, "CLIP_MAP" },
{CommonAssetType::COM_WORLD, "COM_WORLD" },
{CommonAssetType::GAME_WORLD_SP, "GAME_WORLD_SP" },
{CommonAssetType::GAME_WORLD_MP, "GAME_WORLD_MP" },
{CommonAssetType::MAP_ENTS, "MAP_ENTS" },
{CommonAssetType::GFX_WORLD, "GFX_WORLD" },
{CommonAssetType::LIGHT_DEF, "LIGHT_DEF" },
{CommonAssetType::UI_MAP, "UI_MAP" },
{CommonAssetType::FONT, "FONT" },
{CommonAssetType::MENU_LIST, "MENU_LIST" },
{CommonAssetType::MENU, "MENU" },
{CommonAssetType::LOCALIZE_ENTRY, "LOCALIZE_ENTRY" },
{CommonAssetType::WEAPON, "WEAPON" },
{CommonAssetType::SOUND_DRIVER_GLOBALS, "SOUND_DRIVER_GLOBALS"},
{CommonAssetType::FX, "FX" },
{CommonAssetType::IMPACT_FX, "IMPACT_FX" },
{CommonAssetType::AI_TYPE, "AI_TYPE" },
{CommonAssetType::MP_TYPE, "MP_TYPE" },
{CommonAssetType::CHARACTER, "CHARACTER" },
{CommonAssetType::XMODEL_ALIAS, "XMODEL_ALIAS" },
{CommonAssetType::RAW_FILE, "RAW_FILE" },
{CommonAssetType::STRING_TABLE, "STRING_TABLE" },
{CommonAssetType::XMODEL_PIECES, "XMODEL_PIECES" },
{CommonAssetType::PHYS_COLL_MAP, "PHYS_COLL_MAP" },
{CommonAssetType::XMODEL_SURFS, "XMODEL_SURFS" },
{CommonAssetType::PIXEL_SHADER, "PIXEL_SHADER" },
{CommonAssetType::VERTEX_SHADER, "VERTEX_SHADER" },
{CommonAssetType::VERTEX_DECL, "VERTEX_DECL" },
{CommonAssetType::FX_WORLD, "FX_WORLD" },
{CommonAssetType::LEADERBOARD, "LEADERBOARD" },
{CommonAssetType::STRUCTURED_DATA_DEF, "STRUCTURED_DATA_DEF" },
{CommonAssetType::TRACER, "TRACER" },
{CommonAssetType::VEHICLE, "VEHICLE" },
{CommonAssetType::ADDON_MAP_ENTS, "ADDON_MAP_ENTS" },
{CommonAssetType::GLASS_WORLD, "GLASS_WORLD" },
{CommonAssetType::PATH_DATA, "PATH_DATA" },
{CommonAssetType::VEHICLE_TRACK, "VEHICLE_TRACK" },
{CommonAssetType::ATTACHMENT, "ATTACHMENT" },
{CommonAssetType::SURFACE_FX, "SURFACE_FX" },
{CommonAssetType::SCRIPT, "SCRIPT" },
{CommonAssetType::PHYS_CONSTRAINTS, "PHYS_CONSTRAINTS" },
{CommonAssetType::DESTRUCTIBLE_DEF, "DESTRUCTIBLE_DEF" },
{CommonAssetType::SOUND_PATCH, "SOUND_PATCH" },
{CommonAssetType::WEAPON_DEF, "WEAPON_DEF" },
{CommonAssetType::WEAPON_VARIANT, "WEAPON_VARIANT" },
{CommonAssetType::MP_BODY, "MP_BODY" },
{CommonAssetType::MP_HEAD, "MP_HEAD" },
{CommonAssetType::PACK_INDEX, "PACK_INDEX" },
{CommonAssetType::XGLOBALS, "XGLOBALS" },
{CommonAssetType::DDL, "DDL" },
{CommonAssetType::GLASSES, "GLASSES" },
{CommonAssetType::EMBLEM_SET, "EMBLEM_SET" },
{CommonAssetType::FONT_ICON, "FONT_ICON" },
{CommonAssetType::WEAPON_FULL, "WEAPON_FULL" },
{CommonAssetType::ATTACHMENT_UNIQUE, "ATTACHMENT_UNIQUE" },
{CommonAssetType::WEAPON_CAMO, "WEAPON_CAMO" },
{CommonAssetType::KEY_VALUE_PAIRS, "KEY_VALUE_PAIRS" },
{CommonAssetType::MEMORY_BLOCK, "MEMORY_BLOCK" },
{CommonAssetType::SKINNED_VERTS, "SKINNED_VERTS" },
{CommonAssetType::QDB, "QDB" },
{CommonAssetType::SLUG, "SLUG" },
{CommonAssetType::FOOTSTEP_TABLE, "FOOTSTEP_TABLE" },
{CommonAssetType::FOOTSTEP_FX_TABLE, "FOOTSTEP_FX_TABLE" },
{CommonAssetType::ZBARRIER, "ZBARRIER" },
{CommonAssetType::PHYS_PRESET, "phys_preset" },
{CommonAssetType::XANIM, "xanim" },
{CommonAssetType::XMODEL, "xmodel" },
{CommonAssetType::MATERIAL, "material" },
{CommonAssetType::TECHNIQUE_SET, "technique_set" },
{CommonAssetType::IMAGE, "image" },
{CommonAssetType::SOUND, "sound" },
{CommonAssetType::SOUND_CURVE, "sound_curve" },
{CommonAssetType::LOADED_SOUND, "loaded_sound" },
{CommonAssetType::CLIP_MAP, "clip_map" },
{CommonAssetType::COM_WORLD, "com_world" },
{CommonAssetType::GAME_WORLD_SP, "game_world_sp" },
{CommonAssetType::GAME_WORLD_MP, "game_world_mp" },
{CommonAssetType::MAP_ENTS, "map_ents" },
{CommonAssetType::GFX_WORLD, "gfx_world" },
{CommonAssetType::LIGHT_DEF, "light_def" },
{CommonAssetType::UI_MAP, "ui_map" },
{CommonAssetType::FONT, "font" },
{CommonAssetType::MENU_LIST, "menu_list" },
{CommonAssetType::MENU, "menu" },
{CommonAssetType::LOCALIZE_ENTRY, "localize_entry" },
{CommonAssetType::WEAPON, "weapon" },
{CommonAssetType::SOUND_DRIVER_GLOBALS, "sound_driver_globals"},
{CommonAssetType::FX, "fx" },
{CommonAssetType::IMPACT_FX, "impact_fx" },
{CommonAssetType::AI_TYPE, "ai_type" },
{CommonAssetType::MP_TYPE, "mp_type" },
{CommonAssetType::CHARACTER, "character" },
{CommonAssetType::XMODEL_ALIAS, "xmodel_alias" },
{CommonAssetType::RAW_FILE, "raw_file" },
{CommonAssetType::STRING_TABLE, "string_table" },
{CommonAssetType::XMODEL_PIECES, "xmodel_pieces" },
{CommonAssetType::PHYS_COLL_MAP, "phys_coll_map" },
{CommonAssetType::XMODEL_SURFS, "xmodel_surfs" },
{CommonAssetType::PIXEL_SHADER, "pixel_shader" },
{CommonAssetType::VERTEX_SHADER, "vertex_shader" },
{CommonAssetType::VERTEX_DECL, "vertex_decl" },
{CommonAssetType::FX_WORLD, "fx_world" },
{CommonAssetType::LEADERBOARD, "leaderboard" },
{CommonAssetType::STRUCTURED_DATA_DEF, "structured_data_def" },
{CommonAssetType::TRACER, "tracer" },
{CommonAssetType::VEHICLE, "vehicle" },
{CommonAssetType::ADDON_MAP_ENTS, "addon_map_ents" },
{CommonAssetType::GLASS_WORLD, "glass_world" },
{CommonAssetType::PATH_DATA, "path_data" },
{CommonAssetType::VEHICLE_TRACK, "vehicle_track" },
{CommonAssetType::ATTACHMENT, "attachment" },
{CommonAssetType::SURFACE_FX, "surface_fx" },
{CommonAssetType::SCRIPT, "script" },
{CommonAssetType::PHYS_CONSTRAINTS, "phys_constraints" },
{CommonAssetType::DESTRUCTIBLE_DEF, "destructible_def" },
{CommonAssetType::SOUND_PATCH, "sound_patch" },
{CommonAssetType::WEAPON_DEF, "weapon_def" },
{CommonAssetType::WEAPON_VARIANT, "weapon_variant" },
{CommonAssetType::MP_BODY, "mp_body" },
{CommonAssetType::MP_HEAD, "mp_head" },
{CommonAssetType::PACK_INDEX, "pack_index" },
{CommonAssetType::XGLOBALS, "xglobals" },
{CommonAssetType::DDL, "ddl" },
{CommonAssetType::GLASSES, "glasses" },
{CommonAssetType::EMBLEM_SET, "emblem_set" },
{CommonAssetType::FONT_ICON, "font_icon" },
{CommonAssetType::WEAPON_FULL, "weapon_full" },
{CommonAssetType::ATTACHMENT_UNIQUE, "attachment_unique" },
{CommonAssetType::WEAPON_CAMO, "weapon_camo" },
{CommonAssetType::KEY_VALUE_PAIRS, "key_value_pairs" },
{CommonAssetType::MEMORY_BLOCK, "memory_block" },
{CommonAssetType::SKINNED_VERTS, "skinned_verts" },
{CommonAssetType::QDB, "qdb" },
{CommonAssetType::SLUG, "slug" },
{CommonAssetType::FOOTSTEP_TABLE, "footstep_table" },
{CommonAssetType::FOOTSTEP_FX_TABLE, "footstep_fx_table" },
{CommonAssetType::ZBARRIER, "zbarrier" },
});
namespace
@@ -146,11 +146,11 @@ namespace
ZoneAssetsDto result;
{
std::shared_lock lock(context.m_zone_lock);
const auto loadedZones = context.GetLoadedZones();
for (const auto& loadedZone : context.m_loaded_zones)
for (const auto& loadedZone : loadedZones.Data())
{
const auto& zone = *loadedZone->m_zone;
const auto& zone = loadedZone->GetZone();
if (zone.m_name == zoneName)
return CreateZoneAssetsDto(zone);
}
+15 -11
View File
@@ -52,19 +52,23 @@ namespace
std::expected<void, std::string> UnlinkZoneInDbThread(const std::string& zoneName)
{
const auto& context = ModManContext::Get().m_fast_file;
const auto existingZone = std::ranges::find_if(context.m_loaded_zones,
[&zoneName](const std::unique_ptr<LoadedZone>& loadedZone)
{
return loadedZone->m_zone->m_name == zoneName;
});
Zone* zone;
{
auto& context = ModManContext::Get().m_fast_file;
const auto loadedZones = context.GetLoadedZones();
const auto existingZone = std::ranges::find_if(loadedZones.Data(),
[&zoneName](const std::unique_ptr<LoadedZone>& loadedZone)
{
return loadedZone->GetZone().m_name == zoneName;
});
if (existingZone == context.m_loaded_zones.end())
return std::unexpected(std::format("No zone with name {} loaded", zoneName));
if (existingZone == loadedZones.Data().end())
return std::unexpected(std::format("No zone with name {} loaded", zoneName));
const auto& loadedZone = *existingZone->get();
zone = &existingZone->get()->GetZone();
}
auto* objWriter = IObjWriter::GetObjWriterForGame(loadedZone.m_zone->m_game_id);
auto* objWriter = IObjWriter::GetObjWriterForGame(zone->m_game_id);
const auto outputFolderPath = fs::path(utils::GetExecutablePath()).parent_path() / "zone_dump" / zoneName;
const auto outputFolderPathStr = outputFolderPath.string();
@@ -72,7 +76,7 @@ namespace
OutputPathFilesystem outputFolderOutputPath(outputFolderPath);
SearchPaths searchPaths;
AssetDumpingContext dumpingContext(
*loadedZone.m_zone, outputFolderPathStr, outputFolderOutputPath, searchPaths, std::make_unique<UnlinkingEventProgressReporter>(zoneName));
*zone, outputFolderPathStr, outputFolderOutputPath, searchPaths, std::make_unique<UnlinkingEventProgressReporter>(zoneName));
objWriter->DumpZone(dumpingContext);
return {};
+18 -17
View File
@@ -7,19 +7,20 @@
NLOHMANN_JSON_SERIALIZE_ENUM(GameId,
{
{GameId::IW3, "IW3"},
{GameId::IW4, "IW4"},
{GameId::IW5, "IW5"},
{GameId::T4, "T4" },
{GameId::T5, "T5" },
{GameId::T6, "T6" },
{GameId::IW3, "iw3"},
{GameId::IW4, "iw4"},
{GameId::IW5, "iw5"},
{GameId::T4, "t4" },
{GameId::T5, "t5" },
{GameId::T6, "t6" },
});
NLOHMANN_JSON_SERIALIZE_ENUM(GamePlatform,
{
{GamePlatform::PC, "PC" },
{GamePlatform::XBOX, "XBOX"},
{GamePlatform::PS3, "PS3" },
{GamePlatform::PC, "pc" },
{GamePlatform::XBOX, "xbox"},
{GamePlatform::PS3, "ps3" },
{GamePlatform::WIIU, "wiiu"},
});
namespace
@@ -63,10 +64,10 @@ namespace
ZoneDto CreateZoneDto(const LoadedZone& loadedZone)
{
return ZoneDto{
.name = loadedZone.m_zone->m_name,
.filePath = loadedZone.m_file_path,
.game = loadedZone.m_zone->m_game_id,
.platform = loadedZone.m_zone->m_platform,
.name = loadedZone.GetZone().m_name,
.filePath = loadedZone.GetFilePath(),
.game = loadedZone.GetZone().m_game_id,
.platform = loadedZone.GetZone().m_platform,
};
}
@@ -77,10 +78,10 @@ namespace
std::vector<ZoneDto> result;
{
std::shared_lock lock(context.m_zone_lock);
result.reserve(context.m_loaded_zones.size());
const auto loadedZones = context.GetLoadedZones();
result.reserve(loadedZones.Data().size());
for (const auto& loadedZone : context.m_loaded_zones)
for (const auto& loadedZone : loadedZones.Data())
{
result.emplace_back(CreateZoneDto(*loadedZone));
}
@@ -105,7 +106,7 @@ namespace
ZoneLoadedDto{
.zone = CreateZoneDto(*maybeZone.value()),
});
con::debug("Loaded zone \"{}\"", maybeZone.value()->m_zone->m_name);
con::debug("Loaded zone \"{}\"", maybeZone.value()->GetZone().m_name);
}
else
{
+44 -16
View File
@@ -6,6 +6,10 @@
#include "Web/ViteAssets.h"
#include "Web/WebWindowedLib.h"
// Assets
#include "Asset/Image/DynamicAssetsImage.h"
#include "Asset/XModel/DynamicAssetsXModel.h"
#include <format>
#include <iostream>
#include <string>
@@ -19,7 +23,7 @@ using namespace std::string_literals;
namespace
{
#ifdef _DEBUG
void SpawnDevToolsWindow()
void CreateDevToolsWindow()
{
con::debug("Creating dev tools window");
@@ -31,11 +35,19 @@ namespace
newWindow.set_title("Devtools");
newWindow.set_window_size(640, 480);
newWindow.set_window_min(480, 320);
(void)newWindow.navigate(std::format("http://localhost:{}/__devtools__/", VITE_DEV_SERVER_PORT));
const auto result = newWindow.navigate(std::format("http://localhost:{}/__devtools__/", VITE_DEV_SERVER_PORT));
if (!result.has_value())
con::error("Dev tools window navigation failed: {}", result.error().message());
}
#endif
int SpawnMainWindow()
void RegisterDynamicAssets(webwindowed::asset_handler_plugin& assetHandler)
{
image::RegisterDynamicAssets(assetHandler);
xmodel::RegisterDynamicAssets(assetHandler);
}
int RunModManApp()
{
con::debug("Creating main window");
@@ -48,12 +60,21 @@ namespace
#endif
newWindow.set_title("OpenAssetTools ModMan");
// newWindow.set_window_min(640, 480);
newWindow.set_window_min(640, 480);
newWindow.set_window_size(1280, 640);
const auto assetHandlerPlugin = std::make_shared<webwindowed::asset_handler_plugin>(VITE_ASSETS, std::extent_v<decltype(VITE_ASSETS)>);
const auto assetHandlerPlugin = std::make_shared<webwindowed::asset_handler_plugin>();
assetHandlerPlugin->set_protocol_name("modman");
newWindow.register_plugin(assetHandlerPlugin);
#ifdef _DEBUG
// Allow assets from dev server to access dynamic assets
assetHandlerPlugin->set_allow_all_origins(true);
#endif
for (const auto& asset : VITE_ASSETS)
assetHandlerPlugin->add_static_asset(webwindowed::static_asset(asset.filename, asset.data, asset.dataSize));
RegisterDynamicAssets(*assetHandlerPlugin);
webwindowed::commands_builder commands;
ui::RegisterAllBinds(commands);
@@ -62,23 +83,30 @@ namespace
#ifdef _DEBUG
auto result = newWindow.navigate(VITE_DEV_SERVER ? std::format("http://localhost:{}", VITE_DEV_SERVER_PORT)
: assetHandlerPlugin->get_url_for_asset("index.html"));
if (VITE_DEV_SERVER)
{
newWindow.dispatch(
[]
{
SpawnDevToolsWindow();
});
}
#else
auto result = newWindow.navigate(assetHandlerPlugin->get_url_for_asset("index.html"));
#endif
if (!result.has_value())
con::error("Main window navigation failed: {}", result.error().message());
webwindowed::app app;
app.register_plugin(assetHandlerPlugin);
app.register_plugin(std::make_shared<webwindowed::favicon_handler_plugin>());
app.register_plugin(std::make_shared<webwindowed::title_handler_plugin>());
(void)app.run(context.m_main_window);
#ifdef _DEBUG
if (VITE_DEV_SERVER)
{
CreateDevToolsWindow();
result = app.open_window(context.m_dev_tools_window);
if (!result.has_value())
con::error("Failed to open dev tools window: {}", result.error().message());
}
#endif
result = app.run(context.m_main_window);
if (!result.has_value())
con::error("Error while running app: {}", result.error().message());
return 0;
}
@@ -128,7 +156,7 @@ int main(int argc, const char** argv)
ModManContext::Get().Startup();
const auto result = SpawnMainWindow();
const auto result = RunModManApp();
ModManContext::Get().Destroy();
+4 -1
View File
@@ -16,8 +16,10 @@
"dependencies": {
"@fontsource/inter": "5.2.8",
"@primeuix/themes": "2.0.3",
"@vueuse/core": "14.3.0",
"pinia": "3.0.4",
"primevue": "4.5.5",
"three": "0.184.0",
"vue": "3.5.35",
"vue-router": "5.1.0"
},
@@ -25,13 +27,14 @@
"@tsconfig/node24": "24.0.4",
"@types/jsdom": "28.0.3",
"@types/node": "25.9.2",
"@types/three": "0.184.1",
"@vitejs/plugin-vue": "6.0.7",
"@vitest/eslint-plugin": "1.6.19",
"@vue/eslint-config-prettier": "10.2.0",
"@vue/eslint-config-typescript": "14.8.0",
"@vue/test-utils": "2.4.11",
"@vue/tsconfig": "0.9.1",
"@webwindowed/vite-plugin-cpp-header": "1.0.0",
"@webwindowed/vite-plugin-cpp-header": "1.1.0",
"@webwindowed/web-api": "1.0.0",
"eslint": "10.4.1",
"eslint-plugin-vue": "10.9.2",
Binary file not shown.

After

Width:  |  Height:  |  Size: 990 KiB

@@ -0,0 +1 @@
https://polyhaven.com/a/citrus_orchard_puresky
@@ -0,0 +1,169 @@
import { BufferGeometry, Material, Object3D, Texture } from "three";
import { type IUniform } from "three/src/renderers/shaders/UniformsLib.js";
function getMaterialsOfObject(object: Object3D) {
if ("material" in object) {
const value = object.material as Material | Material[];
return Array.isArray(value) ? value : [value];
}
return [];
}
export class ThreeResourceTracker {
private readonly textures: Record<number, number>;
private readonly materials: Record<string, number>;
private readonly geometries: Record<number, number>;
private readonly objects: Record<number, number>;
constructor() {
this.textures = {};
this.materials = {};
this.geometries = {};
this.objects = {};
}
refTexture(texture: Texture) {
if (!this.textures[texture.id]) {
this.textures[texture.id] = 1;
} else {
this.textures[texture.id]++;
}
}
refMaterial(material: Material) {
if (!this.materials[material.uuid]) {
this.materials[material.uuid] = 1;
for (const property of Object.values(material)) {
if (property instanceof Texture) {
this.refTexture(property);
}
if ("uniforms" in material) {
for (const value of Object.values(material.uniforms as Record<string, IUniform>)) {
if (value) {
const uniformValue = value.value;
const uniformValues = Array.isArray(uniformValue) ? uniformValue : [uniformValue];
for (const maybeTexture of uniformValues) {
if (maybeTexture instanceof Texture) {
this.refTexture(maybeTexture);
}
}
}
}
}
}
} else {
this.materials[material.uuid]++;
}
}
refGeometry(geometry: BufferGeometry) {
if (!this.geometries[geometry.id]) {
this.geometries[geometry.id] = 1;
} else {
this.geometries[geometry.id]++;
}
}
refObject(object: Object3D) {
if (!this.objects[object.id]) {
this.objects[object.id] = 1;
for (const material of getMaterialsOfObject(object)) {
this.refMaterial(material);
}
if ("geometry" in object) {
this.refGeometry(object.geometry as BufferGeometry);
}
for (const child of object.children) {
this.refObject(child);
}
} else {
this.objects[object.id]++;
}
}
unrefTexture(texture: Texture) {
const refCount = this.textures[texture.id];
if (refCount === undefined) {
return;
}
if (refCount > 1) {
this.textures[texture.id] = refCount - 1;
} else {
delete this.textures[texture.id];
texture.dispose();
}
}
unrefMaterial(material: Material) {
const refCount = this.materials[material.uuid];
if (refCount === undefined) {
return;
}
if (refCount > 1) {
this.materials[material.uuid] = refCount - 1;
} else {
for (const property of Object.values(material)) {
if (property instanceof Texture) {
this.unrefTexture(property);
}
if ("uniforms" in material) {
for (const value of Object.values(material.uniforms as Record<string, IUniform>)) {
if (value) {
const uniformValue = value.value;
const uniformValues = Array.isArray(uniformValue) ? uniformValue : [uniformValue];
for (const maybeTexture of uniformValues) {
if (maybeTexture instanceof Texture) {
this.unrefTexture(maybeTexture);
}
}
}
}
}
}
material.dispose();
delete this.materials[material.uuid];
}
}
unrefGeometry(geometry: BufferGeometry) {
const refCount = this.geometries[geometry.id];
if (refCount === undefined) {
return;
}
if (refCount > 1) {
this.geometries[geometry.id] = refCount - 1;
} else {
delete this.geometries[geometry.id];
geometry.dispose();
}
}
unrefObject(object: Object3D) {
const refCount = this.objects[object.id];
if (refCount === undefined) {
return;
}
if (refCount > 1) {
this.objects[object.id] = refCount - 1;
} else {
for (const material of getMaterialsOfObject(object)) {
this.unrefMaterial(material);
}
if ("geometry" in object) {
this.unrefGeometry(object.geometry as BufferGeometry);
}
for (const child of object.children) {
this.unrefObject(child);
}
delete this.objects[object.id];
}
}
}
@@ -0,0 +1,249 @@
<script setup lang="ts">
import {
Scene,
PerspectiveCamera,
WebGLRenderer,
AmbientLight,
HemisphereLight,
Box3,
LoadingManager,
Loader,
TextureLoader,
EquirectangularReflectionMapping,
SRGBColorSpace,
type Texture,
} from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader, type GLTF } from "three/addons/loaders/GLTFLoader.js";
import { DDSLoader } from "three/examples/jsm/loaders/DDSLoader.js";
import type { AssetDto } from "@/native/AssetBinds.ts";
import {
computed,
onMounted,
onUnmounted,
ref,
shallowRef,
toRaw,
useTemplateRef,
watch,
} from "vue";
import { useResizeObserver } from "@vueuse/core";
import { ThreeResourceTracker } from "@/components/assets/xmodel/ThreeResourceTracker.ts";
const props = defineProps<{
asset: AssetDto;
zoneName: string;
}>();
const canvasWrapperRef = useTemplateRef<HTMLDivElement>("canvas-wrapper");
const canvasRef = useTemplateRef<HTMLCanvasElement>("canvas");
const scene = new Scene();
const camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const color = 0xffffff;
const intensity = 1;
const light = new AmbientLight(color, intensity);
scene.add(light);
const skyColor = 0xb1e1ff; // light blue
const groundColor = 0xb97a20; // brownish orange
const hemisphereLight = new HemisphereLight(skyColor, groundColor, intensity);
scene.add(hemisphereLight);
camera.position.z = 3;
const IMAGE_REGEX = /\.\.[\\/]images[\\/](.+)\.(.+)$/m;
class ProxyImageLoader extends Loader {
private ddsLoader: DDSLoader;
constructor() {
super();
this.ddsLoader = new DDSLoader();
}
load(
url: string,
onLoad: (data: unknown) => void,
onProgress?: (event: ProgressEvent) => void,
onError?: (err: unknown) => void,
) {
const match = IMAGE_REGEX.exec(url);
if (!match) {
onError?.("invalid url");
return;
}
this.ddsLoader.load(
`modman://localhost/image/dds?zone=${encodeURIComponent(props.zoneName)}&name=${encodeURIComponent(match[1])}`,
onLoad,
onProgress,
onError,
);
}
loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<unknown> {
const match = IMAGE_REGEX.exec(url);
if (!match) {
return Promise.reject("invalid url");
}
return this.ddsLoader.loadAsync(
`modman://localhost/image/dds?zone=${encodeURIComponent(props.zoneName)}&name=${encodeURIComponent(match[1])}`,
onProgress,
);
}
}
const manager = new LoadingManager();
manager.addHandler(IMAGE_REGEX, new ProxyImageLoader());
const gltfLoader = new GLTFLoader(manager);
const modelUri = computed<string>(
() =>
`modman://localhost/xmodel/glb?zone=${encodeURIComponent(props.zoneName)}&name=${encodeURIComponent(props.asset.name)}`,
);
const model = shallowRef<GLTF | undefined>(undefined);
const isLoading = ref(false);
const resourceTracker = new ThreeResourceTracker();
const modelBounds = computed<Box3 | undefined>(() => {
const modelValue = model.value;
if (!modelValue) return undefined;
const box = new Box3();
box.expandByObject(modelValue.scene);
return box;
});
const loader = new TextureLoader();
let skybox: Texture | undefined = undefined;
loader.loadAsync("/skybox/citrus_orchard_puresky.jpg").then((res) => {
skybox = res;
skybox.mapping = EquirectangularReflectionMapping;
skybox.colorSpace = SRGBColorSpace;
scene.background = skybox;
});
watch(
modelUri,
(uri) => {
isLoading.value = true;
gltfLoader
.loadAsync(uri)
.then((gltf) => (model.value = gltf))
.finally(() => (isLoading.value = false));
},
{ immediate: true },
);
let renderer: WebGLRenderer | undefined = undefined;
let controls: OrbitControls | undefined;
function resetCameraPositionForObject() {
const boundsValue = modelBounds.value;
if (!boundsValue) return;
const sizeX = boundsValue.max.x - boundsValue.min.x;
const sizeY = boundsValue.max.y - boundsValue.min.y;
const sizeZ = boundsValue.max.z - boundsValue.min.z;
const middleX = boundsValue.min.x + sizeX / 2;
const middleY = boundsValue.min.y + sizeY / 2;
const middleZ = boundsValue.min.z + sizeZ / 2;
const cameraX = Math.max(sizeY, sizeZ) / 2 + boundsValue.max.x;
camera.position.set(cameraX, middleY, middleZ);
camera.lookAt(middleX, middleY, middleZ);
camera.far = Math.max(cameraX + sizeX, sizeY, sizeZ, 1000) * 2;
camera.updateProjectionMatrix();
if (controls) {
controls.target.set(middleX, middleY, middleZ);
controls.update();
}
}
watch(model, (newVal, oldVal) => {
if (newVal) {
resourceTracker.refObject(newVal.scene);
}
if (oldVal) {
scene.remove(toRaw(oldVal).scene);
}
if (newVal) {
scene.add(toRaw(newVal.scene));
resetCameraPositionForObject();
}
if (oldVal) {
resourceTracker.unrefObject(toRaw(oldVal).scene);
}
});
onMounted(() => {
renderer = new WebGLRenderer({ canvas: canvasRef.value! });
const canvasWrapperBounds = canvasWrapperRef.value!.getBoundingClientRect();
renderer.setSize(canvasWrapperBounds.width, canvasWrapperBounds.height);
function animate() {
renderer!.render(scene, camera);
}
renderer.setAnimationLoop(animate);
controls = new OrbitControls(camera, canvasRef.value!);
controls.target.set(0, 0, 0);
controls.update();
});
onUnmounted(() => {
if (model.value) {
resourceTracker.unrefObject(toRaw(model.value).scene);
}
if (skybox) {
skybox.dispose();
}
});
useResizeObserver(canvasWrapperRef, () => {
const canvasWrapperBounds = canvasWrapperRef.value!.getBoundingClientRect();
renderer?.setSize(canvasWrapperBounds.width, canvasWrapperBounds.height);
});
</script>
<template>
<div class="preview-wrapper" ref="canvas-wrapper">
<canvas class="preview" ref="canvas" />
<div v-if="isLoading" class="loading-overlay">
<span>Loading</span>
</div>
</div>
</template>
<style scoped lang="scss">
.preview-wrapper {
display: flex;
position: relative;
width: 100%;
height: 100%;
}
.preview {
border-radius: 0.25rem;
}
@starting-style {
.loading-overlay {
opacity: 0;
}
}
.loading-overlay {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
transition: opacity ease-in-out 500ms;
}
</style>
+96
View File
@@ -0,0 +1,96 @@
import type { GameId, GamePlatform } from "@/native/ZoneBinds.ts";
import type { CommonAssetType } from "@/native/AssetBinds.ts";
export function localizeGame(game: GameId) {
return game.toUpperCase();
}
const GAME_PLATFORM_LOOKUP: Record<GamePlatform, string> = {
pc: "PC",
ps3: "PS3",
xbox: "Xbox",
wiiu: "WiiU",
};
export function localizePlatform(platform: GamePlatform) {
return GAME_PLATFORM_LOOKUP[platform];
}
const ASSET_TYPE_LOOKUP: Record<CommonAssetType, string> = {
phys_preset: "Phys preset",
xanim: "XAnim",
xmodel: "XModel",
material: "Material",
technique_set: "Technique set",
image: "Image",
sound: "Sound",
sound_curve: "Sound curve",
loaded_sound: "Loaded sound",
clip_map: "Clip map",
com_world: "Com world",
game_world_sp: "Game world SP",
game_world_mp: "Game world MP",
map_ents: "Map ents",
gfx_world: "Gfx world",
light_def: "Light def",
ui_map: "UI map",
font: "Font",
menu_list: "Menu list",
menu: "Menu",
localize_entry: "Localize entry",
weapon: "Weapon",
sound_driver_globals: "Sound driver globals",
fx: "FX",
impact_fx: "Impact FX",
ai_type: "AI type",
mp_type: "MP type",
character: "Character",
xmodel_alias: "XModel alias",
raw_file: "Raw file",
string_table: "String table",
xmodel_pieces: "XModel pieces",
phys_coll_map: "Phys coll map",
xmodel_surfs: "XModel surfs",
pixel_shader: "Pixel shader",
vertex_shader: "Vertex shader",
vertex_decl: "Vertex decl",
fx_world: "FX world",
leaderboard: "Leaderboard",
structured_data_def: "Structured data def",
tracer: "Tracer",
vehicle: "Vehicle",
addon_map_ents: "Addon map ents",
glass_world: "Glass world",
path_data: "Path data",
vehicle_track: "Vehicle track",
attachment: "Attachment",
surface_fx: "Surface FX",
script: "Script",
phys_constraints: "Phys constraints",
destructible_def: "Destructible def",
sound_patch: "Sound patch",
weapon_def: "Weapon def",
weapon_variant: "Weapon variant",
mp_body: "MP body",
mp_head: "MP head",
pack_index: "Pack index",
xglobals: "XGlobals",
ddl: "DDL",
glasses: "Glasses",
emblem_set: "Emblem set",
font_icon: "Font icon",
weapon_full: "Weapon full",
attachment_unique: "Attachment unique",
weapon_camo: "Weapon camo",
key_value_pairs: "Key value pairs",
memory_block: "Memory block",
skinned_verts: "Skinned verts",
qdb: "Qdb",
slug: "Slug",
footstep_table: "Footstep table",
footstep_fx_table: "Footstep FX table",
zbarrier: "ZBarrier",
};
export function localizeAssetType(assetType: CommonAssetType): string {
return ASSET_TYPE_LOOKUP[assetType];
}
+74 -75
View File
@@ -1,80 +1,79 @@
import { getBinds } from "@webwindowed/web-api";
export enum CommonAssetType {
PHYS_PRESET = "PHYS_PRESET",
XANIM = "XANIM",
XMODEL = "XMODEL",
MATERIAL = "MATERIAL",
TECHNIQUE_SET = "TECHNIQUE_SET",
IMAGE = "IMAGE",
SOUND = "SOUND",
SOUND_CURVE = "SOUND_CURVE",
LOADED_SOUND = "LOADED_SOUND",
CLIP_MAP = "CLIP_MAP",
COM_WORLD = "COM_WORLD",
GAME_WORLD_SP = "GAME_WORLD_SP",
GAME_WORLD_MP = "GAME_WORLD_MP",
MAP_ENTS = "MAP_ENTS",
GFX_WORLD = "GFX_WORLD",
LIGHT_DEF = "LIGHT_DEF",
UI_MAP = "UI_MAP",
FONT = "FONT",
MENU_LIST = "MENU_LIST",
MENU = "MENU",
LOCALIZE_ENTRY = "LOCALIZE_ENTRY",
WEAPON = "WEAPON",
SOUND_DRIVER_GLOBALS = "SOUND_DRIVER_GLOBALS",
FX = "FX",
IMPACT_FX = "IMPACT_FX",
AI_TYPE = "AI_TYPE",
MP_TYPE = "MP_TYPE",
CHARACTER = "CHARACTER",
XMODEL_ALIAS = "XMODEL_ALIAS",
RAW_FILE = "RAW_FILE",
STRING_TABLE = "STRING_TABLE",
XMODEL_PIECES = "XMODEL_PIECES",
PHYS_COLL_MAP = "PHYS_COLL_MAP",
XMODEL_SURFS = "XMODEL_SURFS",
PIXEL_SHADER = "PIXEL_SHADER",
VERTEX_SHADER = "VERTEX_SHADER",
VERTEX_DECL = "VERTEX_DECL",
FX_WORLD = "FX_WORLD",
LEADERBOARD = "LEADERBOARD",
STRUCTURED_DATA_DEF = "STRUCTURED_DATA_DEF",
TRACER = "TRACER",
VEHICLE = "VEHICLE",
ADDON_MAP_ENTS = "ADDON_MAP_ENTS",
GLASS_WORLD = "GLASS_WORLD",
PATH_DATA = "PATH_DATA",
VEHICLE_TRACK = "VEHICLE_TRACK",
ATTACHMENT = "ATTACHMENT",
SURFACE_FX = "SURFACE_FX",
SCRIPT = "SCRIPT",
PHYS_CONSTRAINTS = "PHYS_CONSTRAINTS",
DESTRUCTIBLE_DEF = "DESTRUCTIBLE_DEF",
SOUND_PATCH = "SOUND_PATCH",
WEAPON_DEF = "WEAPON_DEF",
WEAPON_VARIANT = "WEAPON_VARIANT",
MP_BODY = "MP_BODY",
MP_HEAD = "MP_HEAD",
PACK_INDEX = "PACK_INDEX",
XGLOBALS = "XGLOBALS",
DDL = "DDL",
GLASSES = "GLASSES",
EMBLEM_SET = "EMBLEM_SET",
FONT_ICON = "FONT_ICON",
WEAPON_FULL = "WEAPON_FULL",
ATTACHMENT_UNIQUE = "ATTACHMENT_UNIQUE",
WEAPON_CAMO = "WEAPON_CAMO",
KEY_VALUE_PAIRS = "KEY_VALUE_PAIRS",
MEMORY_BLOCK = "MEMORY_BLOCK",
SKINNED_VERTS = "SKINNED_VERTS",
QDB = "QDB",
SLUG = "SLUG",
FOOTSTEP_TABLE = "FOOTSTEP_TABLE",
FOOTSTEP_FX_TABLE = "FOOTSTEP_FX_TABLE",
ZBARRIER = "ZBARRIER",
}
export type CommonAssetType =
| "phys_preset"
| "xanim"
| "xmodel"
| "material"
| "technique_set"
| "image"
| "sound"
| "sound_curve"
| "loaded_sound"
| "clip_map"
| "com_world"
| "game_world_sp"
| "game_world_mp"
| "map_ents"
| "gfx_world"
| "light_def"
| "ui_map"
| "font"
| "menu_list"
| "menu"
| "localize_entry"
| "weapon"
| "sound_driver_globals"
| "fx"
| "impact_fx"
| "ai_type"
| "mp_type"
| "character"
| "xmodel_alias"
| "raw_file"
| "string_table"
| "xmodel_pieces"
| "phys_coll_map"
| "xmodel_surfs"
| "pixel_shader"
| "vertex_shader"
| "vertex_decl"
| "fx_world"
| "leaderboard"
| "structured_data_def"
| "tracer"
| "vehicle"
| "addon_map_ents"
| "glass_world"
| "path_data"
| "vehicle_track"
| "attachment"
| "surface_fx"
| "script"
| "phys_constraints"
| "destructible_def"
| "sound_patch"
| "weapon_def"
| "weapon_variant"
| "mp_body"
| "mp_head"
| "pack_index"
| "xglobals"
| "ddl"
| "glasses"
| "emblem_set"
| "font_icon"
| "weapon_full"
| "attachment_unique"
| "weapon_camo"
| "key_value_pairs"
| "memory_block"
| "skinned_verts"
| "qdb"
| "slug"
| "footstep_table"
| "footstep_fx_table"
| "zbarrier";
export interface AssetDto {
type: CommonAssetType;
+2 -13
View File
@@ -1,19 +1,8 @@
import { getBinds } from "@webwindowed/web-api";
export enum GameId {
IW3 = "IW3",
IW4 = "IW4",
IW5 = "IW5",
T4 = "T4",
T5 = "T5",
T6 = "T6",
}
export type GameId = "iw3" | "iw4" | "iw5" | "t4" | "t5" | "t6";
export enum GamePlatform {
PC = "PC",
XBOX = "XBOX",
PS3 = "PS3",
}
export type GamePlatform = "pc" | "xbox" | "ps3" | "wiiu";
export interface ZoneDto {
name: string;
-85
View File
@@ -1,85 +0,0 @@
import { CommonAssetType } from "@/native/AssetBinds";
const LOOKUP_CAPITALIZED: Record<CommonAssetType, string> = {
[CommonAssetType.PHYS_PRESET]: "Phys preset",
[CommonAssetType.XANIM]: "XAnim",
[CommonAssetType.XMODEL]: "XModel",
[CommonAssetType.MATERIAL]: "Material",
[CommonAssetType.TECHNIQUE_SET]: "Technique set",
[CommonAssetType.IMAGE]: "Image",
[CommonAssetType.SOUND]: "Sound",
[CommonAssetType.SOUND_CURVE]: "Sound curve",
[CommonAssetType.LOADED_SOUND]: "Loaded sound",
[CommonAssetType.CLIP_MAP]: "Clip map",
[CommonAssetType.COM_WORLD]: "Com world",
[CommonAssetType.GAME_WORLD_SP]: "Game world SP",
[CommonAssetType.GAME_WORLD_MP]: "Game world MP",
[CommonAssetType.MAP_ENTS]: "Map ents",
[CommonAssetType.GFX_WORLD]: "Gfx world",
[CommonAssetType.LIGHT_DEF]: "Light def",
[CommonAssetType.UI_MAP]: "UI map",
[CommonAssetType.FONT]: "Font",
[CommonAssetType.MENU_LIST]: "Menu list",
[CommonAssetType.MENU]: "Menu",
[CommonAssetType.LOCALIZE_ENTRY]: "Localize entry",
[CommonAssetType.WEAPON]: "Weapon",
[CommonAssetType.SOUND_DRIVER_GLOBALS]: "Sound driver globals",
[CommonAssetType.FX]: "FX",
[CommonAssetType.IMPACT_FX]: "Impact FX",
[CommonAssetType.AI_TYPE]: "AI type",
[CommonAssetType.MP_TYPE]: "MP type",
[CommonAssetType.CHARACTER]: "Character",
[CommonAssetType.XMODEL_ALIAS]: "XModel alias",
[CommonAssetType.RAW_FILE]: "Raw file",
[CommonAssetType.STRING_TABLE]: "String table",
[CommonAssetType.XMODEL_PIECES]: "XModel pieces",
[CommonAssetType.PHYS_COLL_MAP]: "Phys coll map",
[CommonAssetType.XMODEL_SURFS]: "XModel surfs",
[CommonAssetType.PIXEL_SHADER]: "Pixel shader",
[CommonAssetType.VERTEX_SHADER]: "Vertex shader",
[CommonAssetType.VERTEX_DECL]: "Vertex decl",
[CommonAssetType.FX_WORLD]: "FX world",
[CommonAssetType.LEADERBOARD]: "Leaderboard",
[CommonAssetType.STRUCTURED_DATA_DEF]: "Structured data def",
[CommonAssetType.TRACER]: "Tracer",
[CommonAssetType.VEHICLE]: "Vehicle",
[CommonAssetType.ADDON_MAP_ENTS]: "Addon map ents",
[CommonAssetType.GLASS_WORLD]: "Glass world",
[CommonAssetType.PATH_DATA]: "Path data",
[CommonAssetType.VEHICLE_TRACK]: "Vehicle track",
[CommonAssetType.ATTACHMENT]: "Attachment",
[CommonAssetType.SURFACE_FX]: "Surface FX",
[CommonAssetType.SCRIPT]: "Script",
[CommonAssetType.PHYS_CONSTRAINTS]: "Phys constraints",
[CommonAssetType.DESTRUCTIBLE_DEF]: "Destructible def",
[CommonAssetType.SOUND_PATCH]: "Sound patch",
[CommonAssetType.WEAPON_DEF]: "Weapon def",
[CommonAssetType.WEAPON_VARIANT]: "Weapon variant",
[CommonAssetType.MP_BODY]: "MP body",
[CommonAssetType.MP_HEAD]: "MP head",
[CommonAssetType.PACK_INDEX]: "Pack index",
[CommonAssetType.XGLOBALS]: "XGlobals",
[CommonAssetType.DDL]: "DDL",
[CommonAssetType.GLASSES]: "Glasses",
[CommonAssetType.EMBLEM_SET]: "Emblem set",
[CommonAssetType.FONT_ICON]: "Font icon",
[CommonAssetType.WEAPON_FULL]: "Weapon full",
[CommonAssetType.ATTACHMENT_UNIQUE]: "Attachment unique",
[CommonAssetType.WEAPON_CAMO]: "Weapon camo",
[CommonAssetType.KEY_VALUE_PAIRS]: "Key value pairs",
[CommonAssetType.MEMORY_BLOCK]: "Memory block",
[CommonAssetType.SKINNED_VERTS]: "Skinned verts",
[CommonAssetType.QDB]: "Qdb",
[CommonAssetType.SLUG]: "Slug",
[CommonAssetType.FOOTSTEP_TABLE]: "Footstep table",
[CommonAssetType.FOOTSTEP_FX_TABLE]: "Footstep FX table",
[CommonAssetType.ZBARRIER]: "ZBarrier",
};
export function getAssetTypeNameCapitalized(assetType: CommonAssetType): string {
return LOOKUP_CAPITALIZED[assetType];
}
export function getAssetTypeNameLower(assetType: CommonAssetType): string {
return getAssetTypeNameCapitalized(assetType).toLocaleLowerCase();
}
@@ -8,11 +8,11 @@ import type { ZoneDto } from "@/native/ZoneBinds";
import { useZoneStore } from "@/stores/ZoneStore";
import { computed, watch } from "vue";
import type { CommonAssetType } from "@/native/AssetBinds";
import { getAssetTypeNameCapitalized } from "@/utils/AssetTypeName";
import { useRouter } from "vue-router";
import { PAGE } from "@/router/Page";
import { useAssetStore } from "@/stores/AssetStore";
import { storeToRefs } from "pinia";
import { localizeAssetType, localizeGame, localizePlatform } from "@/i18n/i18n.ts";
const assetStore = useAssetStore();
const zoneStore = useZoneStore();
@@ -52,7 +52,7 @@ const meterItems = computed<MeterItem[]>(() => {
.filter((entry) => entry[1] > minItemCountForDisplay)
.sort((e0, e1) => e1[1] - e0[1])
.map((entry) => ({
label: getAssetTypeNameCapitalized(entry[0] as CommonAssetType),
label: localizeAssetType(entry[0] as CommonAssetType),
value: Math.round((entry[1] / assetCount.value) * 100),
}));
@@ -105,8 +105,8 @@ watch(
<h2>{{ selectedZone ?? "No zone selected" }}</h2>
<Button label="Show assets" :disabled="!selectedZone" @click="onClickShowAssets" />
<div v-if="selectedZoneDetails" class="zone-tags">
<Tag :value="selectedZoneDetails.game" />
<Tag :value="selectedZoneDetails.platform" />
<Tag :value="localizeGame(selectedZoneDetails.game)" />
<Tag :value="localizePlatform(selectedZoneDetails.platform)" />
</div>
<div class="zone-assets">
<template v-if="assetsOfZone">
@@ -16,7 +16,7 @@ const props = defineProps<{
zoneName: string;
}>();
const selectedAsset = ref<AssetDto | null>(null);
const selectedAsset = ref<AssetDto | undefined>(undefined);
watch(
() => props.zoneName,
@@ -30,7 +30,7 @@ watch(
<template>
<div class="inspect-details">
<template v-if="assetsOfZone">
<InspectPreview class="inspect-area-preview" />
<InspectPreview :asset="selectedAsset" :zone-name class="inspect-area-preview" />
<InspectAssetDetails :selected-asset="selectedAsset" class="inspect-area-details" />
<InspectZoneAssets
v-model:selected-asset="selectedAsset"
@@ -3,6 +3,7 @@ import { computed } from "vue";
import type { ZoneDto } from "@/native/ZoneBinds.ts";
import { useZoneStore } from "@/stores/ZoneStore.ts";
import Tag from "primevue/tag";
import { localizeGame, localizePlatform } from "@/i18n/i18n.ts";
const zoneStore = useZoneStore();
const props = defineProps<{
@@ -18,8 +19,12 @@ const zoneDetails = computed<ZoneDto | null>(() =>
<span>
<span>Inspect zone: {{ zoneName }}</span>
<template v-if="zoneDetails">
<Tag class="zone-header-tag" :value="zoneDetails.game" severity="secondary" />
<Tag class="zone-header-tag" :value="zoneDetails.platform" severity="secondary" />
<Tag class="zone-header-tag" :value="localizeGame(zoneDetails.game)" severity="secondary" />
<Tag
class="zone-header-tag"
:value="localizePlatform(zoneDetails.platform)"
severity="secondary"
/>
</template>
</span>
</template>
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { AssetDto } from "@/native/AssetBinds.ts";
import { getAssetTypeNameCapitalized } from "@/utils/AssetTypeName.ts";
import { localizeAssetType } from "@/i18n/i18n.ts";
defineProps<{
asset: AssetDto;
@@ -9,7 +9,7 @@ defineProps<{
<template>
<div class="asset-option">
<span class="asset-type">{{ getAssetTypeNameCapitalized(asset.type) }}</span>
<span class="asset-type">{{ localizeAssetType(asset.type) }}</span>
<span class="asset-name">{{ asset.name }}</span>
</div>
</template>
@@ -1,15 +1,15 @@
<script setup lang="ts">
import type { AssetDto } from "@/native/AssetBinds.ts";
import Tag from "primevue/tag";
import { getAssetTypeNameCapitalized } from "@/utils/AssetTypeName.ts";
import { computed } from "vue";
import { localizeAssetType } from "@/i18n/i18n.ts";
const props = defineProps<{
selectedAsset: AssetDto | null;
selectedAsset?: AssetDto;
}>();
const assetTypeName = computed(() =>
props.selectedAsset ? getAssetTypeNameCapitalized(props.selectedAsset.type) : "",
props.selectedAsset ? localizeAssetType(props.selectedAsset.type) : "",
);
</script>
@@ -1,8 +1,17 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import XModelPreview from "@/components/assets/xmodel/XModelPreview.vue";
import type { AssetDto } from "@/native/AssetBinds.ts";
defineProps<{
asset?: AssetDto;
zoneName: string;
}>();
</script>
<template>
<div class="preview">
<span>No preview available</span>
<XModelPreview v-if="asset?.type === 'xmodel'" :asset :zone-name />
<span v-else>No preview available</span>
</div>
</template>
@@ -3,7 +3,7 @@ import Listbox from "primevue/listbox";
import type { AssetDto } from "@/native/AssetBinds.ts";
import AssetListOption from "@/view/inspect_details/components/AssetListOption.vue";
const selectedAsset = defineModel<AssetDto | null>("selectedAsset");
const selectedAsset = defineModel<AssetDto | undefined>("selectedAsset", { required: true });
defineProps<{
assets: AssetDto[];
}>();
+3
View File
@@ -17,6 +17,9 @@ export default defineConfig({
},
},
},
server: {
cors: false,
},
publicDir: fileURLToPath(new URL("./public", import.meta.url)),
plugins: [
vue(),
+84
View File
@@ -0,0 +1,84 @@
#include "AutoSearchPaths.h"
#include "IW3/AutoSearchPathsIW3.h"
#include "IW4/AutoSearchPathsIW4.h"
#include "IW5/AutoSearchPathsIW5.h"
#include "T4/AutoSearchPathsT4.h"
#include "T5/AutoSearchPathsT5.h"
#include "T6/AutoSearchPathsT6.h"
#include "Utils/StringUtils.h"
#include <algorithm>
#include <cassert>
#include <filesystem>
#include <optional>
#include <utility>
namespace fs = std::filesystem;
namespace
{
std::optional<std::string> FindGameRootFolder(const std::string& zoneParentPath, const std::vector<std::string>& zoneDirs)
{
std::string lowerZoneParentPath(zoneParentPath);
utils::MakeStringLowerCase(lowerZoneParentPath);
for (const auto& dir : zoneDirs)
{
std::string normalizedDir(dir);
utils::MakeStringLowerCase(normalizedDir);
if (lowerZoneParentPath.ends_with(normalizedDir) && lowerZoneParentPath[lowerZoneParentPath.size() - dir.size() - 1] == '/')
return zoneParentPath.substr(0, zoneParentPath.size() - dir.size() - 1);
}
return std::nullopt;
}
} // namespace
std::vector<std::string> AutoSearchPaths::GetSearchPathsForZonePath(const std::string& zonePath) const
{
auto folderName = fs::absolute(fs::path(zonePath)).parent_path().string();
std::ranges::replace(folderName, '\\', '/');
const auto maybeGameRootFolder = FindGameRootFolder(folderName, RecognizedZoneDirs());
if (!maybeGameRootFolder)
return {folderName};
std::vector<std::string> result;
const fs::path gameRootFolderPath(*maybeGameRootFolder);
for (const auto& dir : RecognizedZoneDirs())
{
auto dirPath = fs::weakly_canonical(gameRootFolderPath / dir);
if (fs::is_directory(dirPath))
result.emplace_back(dirPath.string());
}
for (const auto& dir : AdditionalSearchPaths())
{
auto dirPath = fs::weakly_canonical(gameRootFolderPath / dir);
if (fs::is_directory(dirPath))
result.emplace_back(dirPath.string());
}
return result;
}
AutoSearchPaths* AutoSearchPaths::GetForGame(GameId gameId)
{
static AutoSearchPaths* autoSearchPaths[]{
new AutoSearchPathsIW3(),
new AutoSearchPathsIW4(),
new AutoSearchPathsIW5(),
new AutoSearchPathsT4(),
new AutoSearchPathsT5(),
new AutoSearchPathsT6(),
};
static_assert(std::extent_v<decltype(autoSearchPaths)> == static_cast<unsigned>(GameId::COUNT));
assert(static_cast<unsigned>(gameId) < static_cast<unsigned>(GameId::COUNT));
return autoSearchPaths[std::to_underlying(gameId)];
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "Game/IGame.h"
#include <string>
#include <vector>
class AutoSearchPaths
{
public:
AutoSearchPaths() = default;
virtual ~AutoSearchPaths() = default;
AutoSearchPaths(const AutoSearchPaths& other) = default;
AutoSearchPaths(AutoSearchPaths&& other) noexcept = default;
AutoSearchPaths& operator=(const AutoSearchPaths& other) = default;
AutoSearchPaths& operator=(AutoSearchPaths&& other) noexcept = default;
std::vector<std::string> GetSearchPathsForZonePath(const std::string& zonePath) const;
static AutoSearchPaths* GetForGame(GameId gameId);
protected:
virtual const std::vector<std::string>& RecognizedZoneDirs() const = 0;
virtual const std::vector<std::string>& AdditionalSearchPaths() const = 0;
};
@@ -0,0 +1,31 @@
#include "AutoSearchPathsIW3.h"
const std::vector<std::string>& AutoSearchPathsIW3::RecognizedZoneDirs() const
{
static std::vector<std::string> recognizedZoneDirs = {
"zone/english",
"zone/french",
"zone/german",
"zone/italian",
"zone/spanish",
"zone/british",
"zone/russian",
"zone/polish",
"zone/korean",
"zone/taiwanese",
"zone/japanese",
"zone/chinese",
"zone/thai",
"zone/leet",
"zone/czech",
};
return recognizedZoneDirs;
}
const std::vector<std::string>& AutoSearchPathsIW3::AdditionalSearchPaths() const
{
static std::vector<std::string> additionalSearchPaths = {
"main",
};
return additionalSearchPaths;
}
@@ -0,0 +1,10 @@
#pragma once
#include "Game/AutoSearchPaths.h"
class AutoSearchPathsIW3 final : public AutoSearchPaths
{
protected:
[[nodiscard]] const std::vector<std::string>& RecognizedZoneDirs() const override;
[[nodiscard]] const std::vector<std::string>& AdditionalSearchPaths() const override;
};
@@ -0,0 +1,37 @@
#include "AutoSearchPathsIW4.h"
const std::vector<std::string>& AutoSearchPathsIW4::RecognizedZoneDirs() const
{
static std::vector<std::string> recognizedZoneDirs = {
"zone/english",
"zone/french",
"zone/german",
"zone/italian",
"zone/spanish",
"zone/british",
"zone/russian",
"zone/polish",
"zone/korean",
"zone/taiwanese",
"zone/japanese",
"zone/chinese",
"zone/thai",
"zone/leet",
"zone/czech",
// Iw4x specific
"zone/patch",
"zone/dlc",
"zone/zonebuilder",
};
return recognizedZoneDirs;
}
const std::vector<std::string>& AutoSearchPathsIW4::AdditionalSearchPaths() const
{
static std::vector<std::string> additionalSearchPaths = {
"main",
"iw4x",
};
return additionalSearchPaths;
}
@@ -0,0 +1,10 @@
#pragma once
#include "Game/AutoSearchPaths.h"
class AutoSearchPathsIW4 final : public AutoSearchPaths
{
protected:
[[nodiscard]] const std::vector<std::string>& RecognizedZoneDirs() const override;
[[nodiscard]] const std::vector<std::string>& AdditionalSearchPaths() const override;
};
@@ -0,0 +1,32 @@
#include "AutoSearchPathsIW5.h"
const std::vector<std::string>& AutoSearchPathsIW5::RecognizedZoneDirs() const
{
static std::vector<std::string> recognizedZoneDirs = {
"zone/english",
"zone/french",
"zone/german",
"zone/italian",
"zone/spanish",
"zone/british",
"zone/russian",
"zone/polish",
"zone/korean",
"zone/taiwanese",
"zone/japanese",
"zone/chinese",
"zone/thai",
"zone/leet",
"zone/czech",
"zone/dlc",
};
return recognizedZoneDirs;
}
const std::vector<std::string>& AutoSearchPathsIW5::AdditionalSearchPaths() const
{
static std::vector<std::string> additionalSearchPaths = {
"main",
};
return additionalSearchPaths;
}
@@ -0,0 +1,10 @@
#pragma once
#include "Game/AutoSearchPaths.h"
class AutoSearchPathsIW5 final : public AutoSearchPaths
{
protected:
[[nodiscard]] const std::vector<std::string>& RecognizedZoneDirs() const override;
[[nodiscard]] const std::vector<std::string>& AdditionalSearchPaths() const override;
};
@@ -0,0 +1,31 @@
#include "AutoSearchPathsT4.h"
const std::vector<std::string>& AutoSearchPathsT4::RecognizedZoneDirs() const
{
static std::vector<std::string> recognizedZoneDirs = {
"zone/English",
"zone/French",
"zone/German",
"zone/Italian",
"zone/Spanish",
"zone/British",
"zone/Russian",
"zone/Polish",
"zone/Korean",
"zone/Taiwanese",
"zone/Japanese",
"zone/Chinese",
"zone/Thai",
"zone/Leet",
"zone/Czech",
};
return recognizedZoneDirs;
}
const std::vector<std::string>& AutoSearchPathsT4::AdditionalSearchPaths() const
{
static std::vector<std::string> additionalSearchPaths = {
"main",
};
return additionalSearchPaths;
}
@@ -0,0 +1,10 @@
#pragma once
#include "Game/AutoSearchPaths.h"
class AutoSearchPathsT4 final : public AutoSearchPaths
{
protected:
[[nodiscard]] const std::vector<std::string>& RecognizedZoneDirs() const override;
[[nodiscard]] const std::vector<std::string>& AdditionalSearchPaths() const override;
};
@@ -0,0 +1,31 @@
#include "AutoSearchPathsT5.h"
const std::vector<std::string>& AutoSearchPathsT5::RecognizedZoneDirs() const
{
static std::vector<std::string> recognizedZoneDirs = {
"zone/Common",
"zone/English",
"zone/French",
"zone/Frenchcan",
"zone/German",
"zone/Austrian",
"zone/Italian",
"zone/Spanish",
"zone/British",
"zone/Russian",
"zone/Polish",
"zone/Korean",
"zone/Japanese",
"zone/Czech",
"zone/Fulljap",
};
return recognizedZoneDirs;
}
const std::vector<std::string>& AutoSearchPathsT5::AdditionalSearchPaths() const
{
static std::vector<std::string> additionalSearchPaths = {
"main",
};
return additionalSearchPaths;
}
@@ -0,0 +1,10 @@
#pragma once
#include "Game/AutoSearchPaths.h"
class AutoSearchPathsT5 final : public AutoSearchPaths
{
protected:
[[nodiscard]] const std::vector<std::string>& RecognizedZoneDirs() const override;
[[nodiscard]] const std::vector<std::string>& AdditionalSearchPaths() const override;
};
@@ -0,0 +1,34 @@
#include "AutoSearchPathsT6.h"
const std::vector<std::string>& AutoSearchPathsT6::RecognizedZoneDirs() const
{
static std::vector<std::string> recognizedZoneDirs = {
"zone/all",
"zone/english",
"zone/french",
"zone/frenchcan",
"zone/german",
"zone/austrian",
"zone/italian",
"zone/spanish",
"zone/british",
"zone/russian",
"zone/polish",
"zone/korean",
"zone/japanese",
"zone/czech",
"zone/fulljap",
"zone/portuguese",
"zone/mexicanspanish",
};
return recognizedZoneDirs;
}
const std::vector<std::string>& AutoSearchPathsT6::AdditionalSearchPaths() const
{
static std::vector<std::string> additionalSearchPaths = {
"main",
"sound",
};
return additionalSearchPaths;
}
@@ -0,0 +1,10 @@
#pragma once
#include "Game/AutoSearchPaths.h"
class AutoSearchPathsT6 final : public AutoSearchPaths
{
protected:
[[nodiscard]] const std::vector<std::string>& RecognizedZoneDirs() const override;
[[nodiscard]] const std::vector<std::string>& AdditionalSearchPaths() const override;
};
+6 -88
View File
@@ -4,6 +4,8 @@
#set DUMPER_HEADER "\"ImageDumper" + GAME + ".h\""
#set CONVERTER_HEADER "\"ImageToCommonConverter" + GAME + ".h\""
#if GAME == "IW3"
#define FEATURE_IW3
#define DX9
@@ -60,104 +62,18 @@
#define IWI_NS iwi27
#endif
#include CONVERTER_HEADER
#include "Image/DdsWriter.h"
#include "Image/ImageCommon.h"
#include "Image/IwiLoader.h"
#include "ObjWriting.h"
#include "Utils/Logging/Log.h"
#include <algorithm>
#include <cassert>
#include <format>
using namespace GAME;
using namespace image;
namespace
{
std::unique_ptr<Texture> LoadImageFromLoadDef(const GfxImage& image)
{
#ifdef DX9
Dx9TextureLoader textureLoader;
#else
Dx12TextureLoader textureLoader;
#endif
const auto& loadDef = *image.texture.loadDef;
#if defined(FEATURE_IW3) || defined(FEATURE_T4)
textureLoader.Width(loadDef.dimensions[0]).Height(loadDef.dimensions[1]).Depth(loadDef.dimensions[2]);
#else
textureLoader.Width(image.width).Height(image.height).Depth(image.depth);
#endif
#if defined(IWI8)
if ((loadDef.flags & image::IWI_NS::IMG_FLAG_MAPTYPE_MASK) == image::IWI_NS::IMG_FLAG_MAPTYPE_3D)
textureLoader.Type(TextureType::T_3D);
else if ((loadDef.flags & image::IWI_NS::IMG_FLAG_MAPTYPE_MASK) == image::IWI_NS::IMG_FLAG_MAPTYPE_CUBE)
textureLoader.Type(TextureType::T_CUBE);
else
textureLoader.Type(TextureType::T_2D);
#else
if (loadDef.flags & image::IWI_NS::IMG_FLAG_VOLMAP)
textureLoader.Type(TextureType::T_3D);
else if (loadDef.flags & image::IWI_NS::IMG_FLAG_CUBEMAP)
textureLoader.Type(TextureType::T_CUBE);
else
textureLoader.Type(TextureType::T_2D);
#endif
#ifdef DX9
textureLoader.Format(static_cast<oat::D3DFORMAT>(loadDef.format));
#else
textureLoader.Format(static_cast<oat::DXGI_FORMAT>(loadDef.format));
#endif
textureLoader.HasMipMaps(!(loadDef.flags & image::IWI_NS::IMG_FLAG_NOMIPMAPS));
return textureLoader.LoadTexture(loadDef.data);
}
std::unique_ptr<Texture> LoadImageFromIwi(const GfxImage& image, ISearchPath& searchPath)
{
#ifdef FEATURE_T6
if (image.streamedPartCount > 0)
{
for (auto* ipak : IIPak::Repository)
{
auto ipakStream = ipak->GetEntryStream(image.hash, image.streamedParts[0].hash);
if (ipakStream)
{
auto loadResult = image::LoadIwi(*ipakStream);
ipakStream->close();
if (loadResult)
return std::move(loadResult->m_texture);
}
}
}
#endif
const auto imageFileName = image::GetFileNameForAsset(image.name, ".iwi");
const auto filePathImage = searchPath.Open(imageFileName);
if (!filePathImage.IsOpen())
{
con::error("Could not find data for image \"{}\"", image.name);
return nullptr;
}
auto loadResult = image::LoadIwi(*filePathImage.m_stream);
return loadResult ? std::move(loadResult->m_texture) : nullptr;
}
std::unique_ptr<Texture> LoadImageData(ISearchPath& searchPath, const GfxImage& image)
{
if (image.texture.loadDef && image.texture.loadDef->resourceSize > 0)
return LoadImageFromLoadDef(image);
return LoadImageFromIwi(image, searchPath);
}
} // namespace
#set CLASS_NAME "Dumper" + GAME
namespace image
@@ -179,10 +95,12 @@ namespace image
}
}
#set CONVERTER_NAME "ToCommonConverter" + GAME
void CLASS_NAME::DumpAsset(AssetDumpingContext& context, const XAssetInfo<AssetImage::Type>& asset)
{
const auto* image = asset.Asset();
const auto texture = LoadImageData(context.m_obj_search_path, *image);
const auto texture = CONVERTER_NAME().Convert(asset, context.m_obj_search_path);
if (!texture)
return;
@@ -0,0 +1,29 @@
#include "ImageToCommonConverter.h"
#include "Game/IW3/Image/ImageToCommonConverterIW3.h"
#include "Game/IW4/Image/ImageToCommonConverterIW4.h"
#include "Game/IW5/Image/ImageToCommonConverterIW5.h"
#include "Game/T4/Image/ImageToCommonConverterT4.h"
#include "Game/T5/Image/ImageToCommonConverterT5.h"
#include "Game/T6/Image/ImageToCommonConverterT6.h"
#include <cassert>
namespace image
{
ToCommonConverter* ToCommonConverter::GetForGame(const GameId gameId)
{
static ToCommonConverter* toCommonConverters[]{
new ToCommonConverterIW3(),
new ToCommonConverterIW4(),
new ToCommonConverterIW5(),
new ToCommonConverterT4(),
new ToCommonConverterT5(),
new ToCommonConverterT6(),
};
static_assert(std::extent_v<decltype(toCommonConverters)> == static_cast<unsigned>(GameId::COUNT));
assert(static_cast<unsigned>(gameId) < static_cast<unsigned>(GameId::COUNT));
return toCommonConverters[std::to_underlying(gameId)];
}
} // namespace image
@@ -0,0 +1,164 @@
#options GAME (IW3, IW4, IW5, T4, T5, T6)
#filename "Game/" + GAME + "/Image/ImageToCommonConverter" + GAME + ".cpp"
#set CONVERTER_HEADER "\"ImageToCommonConverter" + GAME + ".h\""
#if GAME == "IW3"
#define FEATURE_IW3
#define DX9
#define IWI6
#elif GAME == "T4"
#define FEATURE_T4
#define DX9
#define IWI6
#elif GAME == "IW4"
#define FEATURE_IW4
#define DX9
#define IWI8
#elif GAME == "IW5"
#define FEATURE_IW5
#define DX9
#define IWI8
#elif GAME == "T5"
#define FEATURE_T5
#define DX9
#define IWI13
#elif GAME == "T6"
#define FEATURE_T6
#define DX11
#define IWI27
#endif
// This file was templated.
// See ImageToCommonConverter.cpp.template.
// Do not modify, changes will be lost.
#include CONVERTER_HEADER
#ifdef DX9
#include "Image/Dx9TextureLoader.h"
#else
#include "Image/Dx12TextureLoader.h"
#endif
#ifdef FEATURE_T6
#include "ObjContainer/IPak/IPak.h"
#endif
#if defined(IWI6)
#include "Image/IwiWriter6.h"
#define IWI_NS iwi6
#elif defined(IWI8)
#include "Image/IwiWriter8.h"
#define IWI_NS iwi8
#elif defined(IWI13)
#include "Image/IwiWriter13.h"
#define IWI_NS iwi13
#elif defined(IWI27)
#include "Image/IwiWriter27.h"
#define IWI_NS iwi27
#endif
#include "Image/ImageCommon.h"
#include "Image/IwiLoader.h"
#include "ObjWriting.h"
#include "Utils/Logging/Log.h"
#include <algorithm>
#include <format>
using namespace GAME;
using namespace image;
namespace
{
std::unique_ptr<Texture> LoadImageFromLoadDef(const GfxImage& image)
{
#ifdef DX9
Dx9TextureLoader textureLoader;
#else
Dx12TextureLoader textureLoader;
#endif
const auto& loadDef = *image.texture.loadDef;
#if defined(FEATURE_IW3) || defined(FEATURE_T4)
textureLoader.Width(loadDef.dimensions[0]).Height(loadDef.dimensions[1]).Depth(loadDef.dimensions[2]);
#else
textureLoader.Width(image.width).Height(image.height).Depth(image.depth);
#endif
#if defined(IWI8)
if ((loadDef.flags & image::IWI_NS::IMG_FLAG_MAPTYPE_MASK) == image::IWI_NS::IMG_FLAG_MAPTYPE_3D)
textureLoader.Type(TextureType::T_3D);
else if ((loadDef.flags & image::IWI_NS::IMG_FLAG_MAPTYPE_MASK) == image::IWI_NS::IMG_FLAG_MAPTYPE_CUBE)
textureLoader.Type(TextureType::T_CUBE);
else
textureLoader.Type(TextureType::T_2D);
#else
if (loadDef.flags & image::IWI_NS::IMG_FLAG_VOLMAP)
textureLoader.Type(TextureType::T_3D);
else if (loadDef.flags & image::IWI_NS::IMG_FLAG_CUBEMAP)
textureLoader.Type(TextureType::T_CUBE);
else
textureLoader.Type(TextureType::T_2D);
#endif
#ifdef DX9
textureLoader.Format(static_cast<oat::D3DFORMAT>(loadDef.format));
#else
textureLoader.Format(static_cast<oat::DXGI_FORMAT>(loadDef.format));
#endif
textureLoader.HasMipMaps(!(loadDef.flags & image::IWI_NS::IMG_FLAG_NOMIPMAPS));
return textureLoader.LoadTexture(loadDef.data);
}
std::unique_ptr<Texture> LoadImageFromIwi(const GfxImage& image, ISearchPath& searchPath)
{
#ifdef FEATURE_T6
if (image.streamedPartCount > 0)
{
for (auto* ipak : IIPak::Repository)
{
auto ipakStream = ipak->GetEntryStream(image.hash, image.streamedParts[0].hash);
if (ipakStream)
{
auto loadResult = image::LoadIwi(*ipakStream);
ipakStream->close();
if (loadResult)
return std::move(loadResult->m_texture);
}
}
}
#endif
const auto imageFileName = image::GetFileNameForAsset(image.name, ".iwi");
const auto filePathImage = searchPath.Open(imageFileName);
if (!filePathImage.IsOpen())
{
con::error("Could not find data for image \"{}\"", image.name);
return nullptr;
}
auto loadResult = image::LoadIwi(*filePathImage.m_stream);
return loadResult ? std::move(loadResult->m_texture) : nullptr;
}
} // namespace
#set CLASS_NAME "ToCommonConverter" + GAME
namespace image
{
std::unique_ptr<Texture> CLASS_NAME::Convert(const XAssetInfoGeneric& assetInfo, ISearchPath& searchPath)
{
const auto& image = *reinterpret_cast<const XAssetInfo<AssetImage::Type>*>(&assetInfo)->Asset();
if (image.texture.loadDef && image.texture.loadDef->resourceSize > 0)
return LoadImageFromLoadDef(image);
return LoadImageFromIwi(image, searchPath);
}
}
@@ -0,0 +1,25 @@
#pragma once
#include "Image/Texture.h"
#include "Pool/XAssetInfo.h"
#include "SearchPath/ISearchPath.h"
#include <memory>
namespace image
{
class ToCommonConverter
{
public:
ToCommonConverter() = default;
virtual ~ToCommonConverter() = default;
ToCommonConverter(const ToCommonConverter& other) = default;
ToCommonConverter(ToCommonConverter&& other) noexcept = default;
ToCommonConverter& operator=(const ToCommonConverter& other) = default;
ToCommonConverter& operator=(ToCommonConverter&& other) noexcept = default;
static ToCommonConverter* GetForGame(GameId gameId);
virtual std::unique_ptr<Texture> Convert(const XAssetInfoGeneric& assetInfo, ISearchPath& searchPath) = 0;
};
} // namespace image
@@ -0,0 +1,25 @@
#options GAME(IW3, IW4, IW5, T4, T5, T6)
#filename "Game/" + GAME + "/Image/ImageToCommonConverter" + GAME + ".h"
#set GAME_HEADER "\"Game/" + GAME + "/" + GAME + ".h\""
// This file was templated.
// See ImageToCommonConverter.h.template.
// Do not modify, changes will be lost.
#pragma once
#include "Image/ImageToCommonConverter.h"
#include GAME_HEADER
#set CLASS_NAME "ToCommonConverter" + GAME
namespace image
{
class CLASS_NAME final : public ToCommonConverter
{
public:
std::unique_ptr<Texture> Convert(const XAssetInfoGeneric& assetInfo, ISearchPath& searchPath) override;
};
} // namespace xmodel
+90 -541
View File
@@ -5,6 +5,7 @@
#set DUMPER_HEADER "\"XModelDumper" + GAME + ".h\""
#set COMMON_HEADER "\"Game/" + GAME + "/Common" + GAME + ".h\""
#set JSON_HEADER "\"Game/" + GAME + "/XModel/JsonXModel" + GAME + ".h\""
#set CONVERTER_HEADER "\"Game/" + GAME + "/XModel/XModelToCommonConverter" + GAME + ".h\""
#if GAME == "IW3"
#define FEATURE_IW3
@@ -31,6 +32,7 @@
#include COMMON_HEADER
#include JSON_HEADER
#include CONVERTER_HEADER
#include "ObjWriting.h"
#include "Utils/DistinctMapper.h"
@@ -48,251 +50,8 @@
using namespace GAME;
#set CLASS_NAME "Dumper" + GAME
namespace
{
std::string GetFileNameForLod(const std::string& modelName, const unsigned lod, const std::string& extension)
{
return std::format("model_export/{}_lod{}{}", modelName, lod, extension);
}
GfxImage* GetImageFromTextureDef(const MaterialTextureDef& textureDef)
{
#ifdef FEATURE_T6
return textureDef.image;
#else
return textureDef.u.image;
#endif
}
GfxImage* GetMaterialColorMap(const Material* material)
{
std::vector<MaterialTextureDef*> potentialTextureDefs;
for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
{
MaterialTextureDef* def = &material->textureTable[textureIndex];
#if defined(FEATURE_IW3) || defined(FEATURE_IW4) || defined(FEATURE_IW5)
if (def->semantic == TS_COLOR_MAP)
potentialTextureDefs.push_back(def);
#else
if (def->semantic == TS_COLOR_MAP || def->semantic >= TS_COLOR0_MAP && def->semantic <= TS_COLOR15_MAP)
potentialTextureDefs.push_back(def);
#endif
}
if (potentialTextureDefs.empty())
return nullptr;
if (potentialTextureDefs.size() == 1)
return GetImageFromTextureDef(*potentialTextureDefs[0]);
for (const auto* def : potentialTextureDefs)
{
if (tolower(def->nameStart) == 'c' && tolower(def->nameEnd) == 'p')
return GetImageFromTextureDef(*def);
}
for (const auto* def : potentialTextureDefs)
{
if (tolower(def->nameStart) == 'r' && tolower(def->nameEnd) == 'k')
return GetImageFromTextureDef(*def);
}
for (const auto* def : potentialTextureDefs)
{
if (tolower(def->nameStart) == 'd' && tolower(def->nameEnd) == 'p')
return GetImageFromTextureDef(*def);
}
return GetImageFromTextureDef(*potentialTextureDefs[0]);
}
GfxImage* GetMaterialNormalMap(const Material* material)
{
std::vector<MaterialTextureDef*> potentialTextureDefs;
for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
{
MaterialTextureDef* def = &material->textureTable[textureIndex];
if (def->semantic == TS_NORMAL_MAP)
potentialTextureDefs.push_back(def);
}
if (potentialTextureDefs.empty())
return nullptr;
if (potentialTextureDefs.size() == 1)
return GetImageFromTextureDef(*potentialTextureDefs[0]);
for (const auto* def : potentialTextureDefs)
{
if (def->nameStart == 'n' && def->nameEnd == 'p')
return GetImageFromTextureDef(*def);
}
return GetImageFromTextureDef(*potentialTextureDefs[0]);
}
GfxImage* GetMaterialSpecularMap(const Material* material)
{
std::vector<MaterialTextureDef*> potentialTextureDefs;
for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
{
MaterialTextureDef* def = &material->textureTable[textureIndex];
if (def->semantic == TS_SPECULAR_MAP)
potentialTextureDefs.push_back(def);
}
if (potentialTextureDefs.empty())
return nullptr;
if (potentialTextureDefs.size() == 1)
return GetImageFromTextureDef(*potentialTextureDefs[0]);
for (const auto* def : potentialTextureDefs)
{
if (def->nameStart == 's' && def->nameEnd == 'p')
return GetImageFromTextureDef(*def);
}
return GetImageFromTextureDef(*potentialTextureDefs[0]);
}
bool GetSurfaces(const XModel& model, const unsigned lod, XSurface*& surfs, unsigned& surfCount)
{
#if defined(FEATURE_IW4) || defined(FEATURE_IW5)
if (!model.lodInfo[lod].modelSurfs || !model.lodInfo[lod].modelSurfs->surfs)
return false;
surfs = model.lodInfo[lod].modelSurfs->surfs;
surfCount = model.lodInfo[lod].modelSurfs->numsurfs;
#else
if (!model.surfs)
return false;
surfs = &model.surfs[model.lodInfo[lod].surfIndex];
surfCount = model.lodInfo[lod].numsurfs;
#endif
return true;
}
bool HasDefaultArmatureForLod(const XModel& model, const unsigned lod)
{
if (model.numRootBones != 1 || model.numBones != 1)
return false;
XSurface* surfs;
unsigned surfCount;
if (!GetSurfaces(model, lod, surfs, surfCount))
return true;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
const auto& surface = surfs[surfIndex];
if (surface.vertListCount != 1 || surface.vertInfo.vertsBlend)
return false;
const auto& vertList = surface.vertList[0];
#ifdef FEATURE_IW3
// IW3 has some models that are missing 1 (a single) tri in its first lod.
// It is not contained in any vert list or blend
// I think this is a bug (?), so omit anyway.
// The "one tri missing" is not supported by the exporter anyway.
if (vertList.boneOffset != 0 || vertList.triOffset != 0 || vertList.vertCount != surface.vertCount)
#else
if (vertList.boneOffset != 0 || vertList.triOffset != 0 || vertList.triCount != surface.triCount || vertList.vertCount != surface.vertCount)
#endif
return false;
}
return true;
}
bool HasDefaultArmatureForAllLods(const XModel& model)
{
for (auto lod = 0u; lod < model.numLods; lod++)
{
if (!HasDefaultArmatureForLod(model, lod))
return false;
}
return true;
}
void OmitDefaultArmature(XModelCommon& common)
{
common.m_bones.clear();
common.m_bone_weight_data.weights.clear();
common.m_vertex_bone_weights.resize(common.m_vertices.size());
for (auto& vertexWeights : common.m_vertex_bone_weights)
{
vertexWeights.weightOffset = 0u;
vertexWeights.weightCount = 0u;
}
}
void AddXModelBones(XModelCommon& out, const AssetDumpingContext& context, const XModel& model)
{
for (auto boneNum = 0u; boneNum < model.numBones; boneNum++)
{
XModelBone bone;
if (model.boneNames[boneNum] < context.m_zone.m_script_strings.Count())
bone.name = context.m_zone.m_script_strings[model.boneNames[boneNum]];
else
bone.name = "INVALID_BONE_NAME";
if (boneNum >= model.numRootBones)
bone.parentIndex = static_cast<int>(boneNum - static_cast<unsigned int>(model.parentList[boneNum - model.numRootBones]));
else
bone.parentIndex = std::nullopt;
bone.scale[0] = 1.0f;
bone.scale[1] = 1.0f;
bone.scale[2] = 1.0f;
const auto& baseMat = model.baseMat[boneNum];
bone.globalOffset[0] = baseMat.trans.x;
bone.globalOffset[1] = baseMat.trans.y;
bone.globalOffset[2] = baseMat.trans.z;
bone.globalRotation = {
.x = baseMat.quat.x,
.y = baseMat.quat.y,
.z = baseMat.quat.z,
.w = baseMat.quat.w,
};
if (boneNum < model.numRootBones)
{
bone.localOffset[0] = 0;
bone.localOffset[1] = 0;
bone.localOffset[2] = 0;
bone.localRotation = {.x = 0, .y = 0, .z = 0, .w = 1};
}
else
{
const auto* trans = &model.trans[(boneNum - model.numRootBones) * 3];
bone.localOffset[0] = trans[0];
bone.localOffset[1] = trans[1];
bone.localOffset[2] = trans[2];
const auto& quat = model.quats[boneNum - model.numRootBones];
bone.localRotation = {
.x = QuatInt16::ToFloat(quat.v[0]),
.y = QuatInt16::ToFloat(quat.v[1]),
.z = QuatInt16::ToFloat(quat.v[2]),
.w = QuatInt16::ToFloat(quat.v[3]),
};
}
out.m_bones.emplace_back(std::move(bone));
}
}
const char* AssetName(const char* input)
{
if (input && input[0] == ',')
@@ -300,297 +59,10 @@ namespace
return input;
}
void AddXModelMaterials(XModelCommon& out, DistinctMapper<Material*>& materialMapper, const XModel& model)
std::string GetFileNameForLod(const std::string& modelName, const unsigned lod, const std::string& extension)
{
for (auto surfaceMaterialNum = 0u; surfaceMaterialNum < model.numsurfs; surfaceMaterialNum++)
{
Material* material = model.materialHandles[surfaceMaterialNum];
if (materialMapper.Add(material))
{
XModelMaterial xMaterial;
xMaterial.ApplyDefaults();
xMaterial.name = AssetName(material->info.name);
const auto* colorMap = GetMaterialColorMap(material);
if (colorMap)
xMaterial.colorMapName = AssetName(colorMap->name);
const auto* normalMap = GetMaterialNormalMap(material);
if (normalMap)
xMaterial.normalMapName = AssetName(normalMap->name);
const auto* specularMap = GetMaterialSpecularMap(material);
if (specularMap)
xMaterial.specularMapName = AssetName(specularMap->name);
out.m_materials.emplace_back(std::move(xMaterial));
}
}
}
void AddXModelObjects(XModelCommon& out, const XModel& model, const unsigned lod, const DistinctMapper<Material*>& materialMapper)
{
const auto surfCount = model.lodInfo[lod].numsurfs;
const auto baseSurfaceIndex = model.lodInfo[lod].surfIndex;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
XModelObject object;
object.name = std::format("surf{}", surfIndex);
object.materialIndex = static_cast<int>(materialMapper.GetDistinctPositionByInputPosition(surfIndex + baseSurfaceIndex));
out.m_objects.emplace_back(std::move(object));
}
}
void AddXModelVertices(XModelCommon& out, const XModel& model, const unsigned lod)
{
XSurface* surfs;
unsigned surfCount;
if (!GetSurfaces(model, lod, surfs, surfCount))
return;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
const auto& surface = surfs[surfIndex];
for (auto vertexIndex = 0u; vertexIndex < surface.vertCount; vertexIndex++)
{
const auto& v = surface.verts0[vertexIndex];
XModelVertex vertex{};
vertex.coordinates[0] = v.xyz.x;
vertex.coordinates[1] = v.xyz.y;
vertex.coordinates[2] = v.xyz.z;
Common::Vec3UnpackUnitVec(v.normal, vertex.normal);
Common::Vec4UnpackGfxColor(v.color, vertex.color);
Common::Vec2UnpackTexCoords(v.texCoord, vertex.uv);
out.m_vertices.emplace_back(vertex);
}
}
}
void AllocateXModelBoneWeights(const XModel& model, const unsigned lod, XModelVertexBoneWeightCollection& weightCollection)
{
XSurface* surfs;
unsigned surfCount;
if (!GetSurfaces(model, lod, surfs, surfCount))
return;
auto totalWeightCount = 0u;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
const auto& surface = surfs[surfIndex];
if (surface.vertList)
{
totalWeightCount += surface.vertListCount;
}
if (surface.vertInfo.vertsBlend)
{
totalWeightCount += surface.vertInfo.vertCount[0] * 1;
totalWeightCount += surface.vertInfo.vertCount[1] * 2;
totalWeightCount += surface.vertInfo.vertCount[2] * 3;
totalWeightCount += surface.vertInfo.vertCount[3] * 4;
}
}
weightCollection.weights.resize(totalWeightCount);
}
float BoneWeight16(const uint16_t value)
{
return static_cast<float>(value) / static_cast<float>(std::numeric_limits<uint16_t>::max());
}
void AddXModelVertexBoneWeights(XModelCommon& out, const XModel& model, const unsigned lod)
{
XSurface* surfs;
unsigned surfCount;
if (!GetSurfaces(model, lod, surfs, surfCount))
return;
auto& weightCollection = out.m_bone_weight_data;
auto weightOffset = 0u;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
const auto& surface = surfs[surfIndex];
auto handledVertices = 0u;
if (surface.vertList)
{
#if defined(FEATURE_IW3) || defined(FEATURE_IW4)
assert(!surface.deformed);
#else
assert((surface.flags & XSURFACE_FLAG_DEFORMED) == 0);
#endif
for (auto vertListIndex = 0u; vertListIndex < surface.vertListCount; vertListIndex++)
{
const auto& vertList = surface.vertList[vertListIndex];
const auto boneWeightOffset = weightOffset;
weightCollection.weights[weightOffset++] =
XModelBoneWeight{.boneIndex = static_cast<unsigned>(vertList.boneOffset / sizeof(DObjSkelMat)), .weight = 1.0f};
for (auto vertListVertexOffset = 0u; vertListVertexOffset < vertList.vertCount; vertListVertexOffset++)
{
out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
}
handledVertices += vertList.vertCount;
}
}
auto vertsBlendOffset = 0u;
if (surface.vertInfo.vertsBlend)
{
#if defined(FEATURE_IW3) || defined(FEATURE_IW4)
assert(surface.deformed);
#else
assert((surface.flags & XSURFACE_FLAG_DEFORMED) > 0);
#endif
// 1 bone weight
for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[0]; vertIndex++)
{
const auto boneWeightOffset = weightOffset;
const unsigned boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex0, .weight = 1.0f};
vertsBlendOffset += 1;
out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
}
// 2 bone weights
for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[1]; vertIndex++)
{
const auto boneWeightOffset = weightOffset;
const unsigned boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
const unsigned boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
const auto boneWeight0 = 1.0f - boneWeight1;
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex0, .weight = boneWeight0};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex1, .weight = boneWeight1};
vertsBlendOffset += 3;
out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 2);
}
// 3 bone weights
for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[2]; vertIndex++)
{
const auto boneWeightOffset = weightOffset;
const unsigned boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
const unsigned boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
const unsigned boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2;
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex0, .weight = boneWeight0};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex1, .weight = boneWeight1};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex2, .weight = boneWeight2};
vertsBlendOffset += 5;
out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 3);
}
// 4 bone weights
for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[3]; vertIndex++)
{
const auto boneWeightOffset = weightOffset;
const unsigned boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
const unsigned boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
const unsigned boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
const unsigned boneIndex3 = surface.vertInfo.vertsBlend[vertsBlendOffset + 5] / sizeof(DObjSkelMat);
const auto boneWeight3 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 6]);
const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2 - boneWeight3;
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex0, .weight = boneWeight0};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex1, .weight = boneWeight1};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex2, .weight = boneWeight2};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex3, .weight = boneWeight3};
vertsBlendOffset += 7;
out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 4);
}
handledVertices +=
surface.vertInfo.vertCount[0] + surface.vertInfo.vertCount[1] + surface.vertInfo.vertCount[2] + surface.vertInfo.vertCount[3];
}
for (; handledVertices < surface.vertCount; handledVertices++)
{
out.m_vertex_bone_weights.emplace_back(0, 0);
}
}
}
void AddXModelFaces(XModelCommon& out, const XModel& model, const unsigned lod)
{
XSurface* surfs;
unsigned surfCount;
if (!GetSurfaces(model, lod, surfs, surfCount))
return;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
const auto& surface = surfs[surfIndex];
auto& object = out.m_objects[surfIndex];
object.m_faces.reserve(surface.triCount);
for (auto triIndex = 0u; triIndex < surface.triCount; triIndex++)
{
const auto& tri = surface.triIndices[triIndex];
XModelFace face{};
face.vertexIndex[0] = tri.i[0] + surface.baseVertIndex;
face.vertexIndex[1] = tri.i[1] + surface.baseVertIndex;
face.vertexIndex[2] = tri.i[2] + surface.baseVertIndex;
object.m_faces.emplace_back(face);
}
}
}
bool CanOmitDefaultArmature()
{
return ObjWriting::Configuration.ModelOutputFormat != ModelOutputFormat_e::XMODEL_EXPORT
&& ObjWriting::Configuration.ModelOutputFormat != ModelOutputFormat_e::XMODEL_BIN;
}
void PopulateXModelWriter(XModelCommon& out, const AssetDumpingContext& context, const unsigned lod, const XModel& model)
{
DistinctMapper<Material*> materialMapper(model.numsurfs);
AllocateXModelBoneWeights(model, lod, out.m_bone_weight_data);
out.m_name = std::format("{}_lod{}", model.name, lod);
AddXModelMaterials(out, materialMapper, model);
AddXModelObjects(out, model, lod, materialMapper);
AddXModelVertices(out, model, lod);
AddXModelFaces(out, model, lod);
// Keep armature handling consistent across all LODs so dumped GLTF/GLB round-trips
// preserve the same bone layout when re-imported.
if (!CanOmitDefaultArmature() || !HasDefaultArmatureForAllLods(model))
{
AddXModelBones(out, context, model);
AddXModelVertexBoneWeights(out, model, lod);
}
else
{
OmitDefaultArmature(out);
}
return std::format("model_export/{}_lod{}{}", modelName, lod, extension);
}
void DumpObjMtl(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>& asset)
@@ -667,37 +139,43 @@ namespace
writer->Write(common);
}
#set CONVERTER_NAME "ToCommonConverter" + GAME
void DumpXModelSurfs(const AssetDumpingContext& context, const XAssetInfo<XModel>& asset)
{
const auto& model = *asset.Asset();
for (auto currentLod = 0u; currentLod < model.numLods; currentLod++)
{
XModelCommon common;
PopulateXModelWriter(common, context, currentLod, model);
const auto maybeCommon = xmodel::CONVERTER_NAME().Convert(asset, currentLod);
if (!maybeCommon)
{
con::warn("Failed to convert to convert xmodel \"{}\" (lod: {})", model.name, currentLod);
continue;
}
switch (ObjWriting::Configuration.ModelOutputFormat)
{
case ModelOutputFormat_e::OBJ:
DumpObjLod(common, context, asset, currentLod);
DumpObjLod(*maybeCommon, context, asset, currentLod);
if (currentLod == 0u)
DumpObjMtl(common, context, asset);
DumpObjMtl(*maybeCommon, context, asset);
break;
case ModelOutputFormat_e::XMODEL_EXPORT:
DumpXModelExportLod(common, context, asset, currentLod);
DumpXModelExportLod(*maybeCommon, context, asset, currentLod);
break;
case ModelOutputFormat_e::XMODEL_BIN:
DumpXModelBinLod(common, context, asset, currentLod);
DumpXModelBinLod(*maybeCommon, context, asset, currentLod);
break;
case ModelOutputFormat_e::GLTF:
DumpGltfLod<gltf::TextOutput>(common, context, asset, currentLod, ".gltf");
DumpGltfLod<gltf::TextOutput>(*maybeCommon, context, asset, currentLod, ".gltf");
break;
case ModelOutputFormat_e::GLB:
DumpGltfLod<gltf::BinOutput>(common, context, asset, currentLod, ".glb");
DumpGltfLod<gltf::BinOutput>(*maybeCommon, context, asset, currentLod, ".glb");
break;
default:
@@ -815,6 +293,75 @@ namespace
if (rootBoneName != xmodel::DEFAULT_XMODEL_ROOT_BONE_NAME)
jXModel.rootBoneName = rootBoneName;
}
bool GetSurfaces(const XModel& model, const unsigned lod, XSurface*& surfs, unsigned& surfCount)
{
#if defined(FEATURE_IW4) || defined(FEATURE_IW5)
if (!model.lodInfo[lod].modelSurfs || !model.lodInfo[lod].modelSurfs->surfs)
return false;
surfs = model.lodInfo[lod].modelSurfs->surfs;
surfCount = model.lodInfo[lod].modelSurfs->numsurfs;
#else
if (!model.surfs)
return false;
surfs = &model.surfs[model.lodInfo[lod].surfIndex];
surfCount = model.lodInfo[lod].numsurfs;
#endif
return true;
}
bool HasDefaultArmatureForLod(const XModel& model, const unsigned lod)
{
if (model.numRootBones != 1 || model.numBones != 1)
return false;
XSurface* surfs;
unsigned surfCount;
if (!GetSurfaces(model, lod, surfs, surfCount))
return true;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
const auto& surface = surfs[surfIndex];
if (surface.vertListCount != 1 || surface.vertInfo.vertsBlend)
return false;
const auto& vertList = surface.vertList[0];
#ifdef FEATURE_IW3
// IW3 has some models that are missing 1 (a single) tri in its first lod.
// It is not contained in any vert list or blend
// I think this is a bug (?), so omit anyway.
// The "one tri missing" is not supported by the exporter anyway.
if (vertList.boneOffset != 0 || vertList.triOffset != 0 || vertList.vertCount != surface.vertCount)
#else
if (vertList.boneOffset != 0 || vertList.triOffset != 0 || vertList.triCount != surface.triCount || vertList.vertCount != surface.vertCount)
#endif
return false;
}
return true;
}
bool HasDefaultArmatureForAllLods(const XModel& model)
{
for (auto lod = 0u; lod < model.numLods; lod++)
{
if (!HasDefaultArmatureForLod(model, lod))
return false;
}
return true;
}
bool CanOmitDefaultArmature()
{
return ObjWriting::Configuration.ModelOutputFormat != ModelOutputFormat_e::XMODEL_EXPORT
&& ObjWriting::Configuration.ModelOutputFormat != ModelOutputFormat_e::XMODEL_BIN;
}
void CreateJsonXModel(AssetDumpingContext& context, JsonXModel& jXModel, const XModel& model)
{
@@ -884,6 +431,8 @@ namespace
}
} // namespace
#set CLASS_NAME "Dumper" + GAME
namespace xmodel
{
void CLASS_NAME::DumpAsset(AssetDumpingContext& context, const XAssetInfo<AssetXModel::Type>& asset)
@@ -0,0 +1,28 @@
#include "XModelToCommonConverter.h"
#include "Game/IW3/XModel/XModelToCommonConverterIW3.h"
#include "Game/IW4/XModel/XModelToCommonConverterIW4.h"
#include "Game/IW5/XModel/XModelToCommonConverterIW5.h"
#include "Game/T5/XModel/XModelToCommonConverterT5.h"
#include "Game/T6/XModel/XModelToCommonConverterT6.h"
#include <cassert>
namespace xmodel
{
ToCommonConverter* ToCommonConverter::GetForGame(const GameId gameId)
{
static ToCommonConverter* toCommonConverters[]{
new ToCommonConverterIW3(),
new ToCommonConverterIW4(),
new ToCommonConverterIW5(),
nullptr,
new ToCommonConverterT5(),
new ToCommonConverterT6(),
};
static_assert(std::extent_v<decltype(toCommonConverters)> == static_cast<unsigned>(GameId::COUNT));
assert(static_cast<unsigned>(gameId) < static_cast<unsigned>(GameId::COUNT));
return toCommonConverters[std::to_underlying(gameId)];
}
} // namespace xmodel
@@ -0,0 +1,588 @@
#options GAME (IW3, IW4, IW5, T5, T6)
#filename "Game/" + GAME + "/XModel/XModelToCommonConverter" + GAME + ".cpp"
#set CONVERTER_HEADER "\"XModelToCommonConverter" + GAME + ".h\""
#set COMMON_HEADER "\"Game/" + GAME + "/Common" + GAME + ".h\""
#if GAME == "IW3"
#define FEATURE_IW3
#elif GAME == "IW4"
#define FEATURE_IW4
#elif GAME == "IW5"
#define FEATURE_IW5
#elif GAME == "T5"
#define FEATURE_T5
#elif GAME == "T6"
#define FEATURE_T6
#endif
// This file was templated.
// See XModelToCommonConverter.cpp.template.
// Do not modify, changes will be lost.
#include CONVERTER_HEADER
#include COMMON_HEADER
#include "ObjWriting.h"
#include "Utils/DistinctMapper.h"
#include "Utils/QuatInt16.h"
#include "XModel/XModelWriter.h"
#include <cassert>
#include <format>
using namespace GAME;
namespace
{
GfxImage* GetImageFromTextureDef(const MaterialTextureDef& textureDef)
{
#ifdef FEATURE_T6
return textureDef.image;
#else
return textureDef.u.image;
#endif
}
GfxImage* GetMaterialColorMap(const Material* material)
{
std::vector<MaterialTextureDef*> potentialTextureDefs;
for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
{
MaterialTextureDef* def = &material->textureTable[textureIndex];
#if defined(FEATURE_IW3) || defined(FEATURE_IW4) || defined(FEATURE_IW5)
if (def->semantic == TS_COLOR_MAP)
potentialTextureDefs.push_back(def);
#else
if (def->semantic == TS_COLOR_MAP || def->semantic >= TS_COLOR0_MAP && def->semantic <= TS_COLOR15_MAP)
potentialTextureDefs.push_back(def);
#endif
}
if (potentialTextureDefs.empty())
return nullptr;
if (potentialTextureDefs.size() == 1)
return GetImageFromTextureDef(*potentialTextureDefs[0]);
for (const auto* def : potentialTextureDefs)
{
if (tolower(def->nameStart) == 'c' && tolower(def->nameEnd) == 'p')
return GetImageFromTextureDef(*def);
}
for (const auto* def : potentialTextureDefs)
{
if (tolower(def->nameStart) == 'r' && tolower(def->nameEnd) == 'k')
return GetImageFromTextureDef(*def);
}
for (const auto* def : potentialTextureDefs)
{
if (tolower(def->nameStart) == 'd' && tolower(def->nameEnd) == 'p')
return GetImageFromTextureDef(*def);
}
return GetImageFromTextureDef(*potentialTextureDefs[0]);
}
GfxImage* GetMaterialNormalMap(const Material* material)
{
std::vector<MaterialTextureDef*> potentialTextureDefs;
for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
{
MaterialTextureDef* def = &material->textureTable[textureIndex];
if (def->semantic == TS_NORMAL_MAP)
potentialTextureDefs.push_back(def);
}
if (potentialTextureDefs.empty())
return nullptr;
if (potentialTextureDefs.size() == 1)
return GetImageFromTextureDef(*potentialTextureDefs[0]);
for (const auto* def : potentialTextureDefs)
{
if (def->nameStart == 'n' && def->nameEnd == 'p')
return GetImageFromTextureDef(*def);
}
return GetImageFromTextureDef(*potentialTextureDefs[0]);
}
GfxImage* GetMaterialSpecularMap(const Material* material)
{
std::vector<MaterialTextureDef*> potentialTextureDefs;
for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
{
MaterialTextureDef* def = &material->textureTable[textureIndex];
if (def->semantic == TS_SPECULAR_MAP)
potentialTextureDefs.push_back(def);
}
if (potentialTextureDefs.empty())
return nullptr;
if (potentialTextureDefs.size() == 1)
return GetImageFromTextureDef(*potentialTextureDefs[0]);
for (const auto* def : potentialTextureDefs)
{
if (def->nameStart == 's' && def->nameEnd == 'p')
return GetImageFromTextureDef(*def);
}
return GetImageFromTextureDef(*potentialTextureDefs[0]);
}
bool GetSurfaces(const XModel& model, const unsigned lod, XSurface*& surfs, unsigned& surfCount)
{
#if defined(FEATURE_IW4) || defined(FEATURE_IW5)
if (!model.lodInfo[lod].modelSurfs || !model.lodInfo[lod].modelSurfs->surfs)
return false;
surfs = model.lodInfo[lod].modelSurfs->surfs;
surfCount = model.lodInfo[lod].modelSurfs->numsurfs;
#else
if (!model.surfs)
return false;
surfs = &model.surfs[model.lodInfo[lod].surfIndex];
surfCount = model.lodInfo[lod].numsurfs;
#endif
return true;
}
bool HasDefaultArmatureForLod(const XModel& model, const unsigned lod)
{
if (model.numRootBones != 1 || model.numBones != 1)
return false;
XSurface* surfs;
unsigned surfCount;
if (!GetSurfaces(model, lod, surfs, surfCount))
return true;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
const auto& surface = surfs[surfIndex];
if (surface.vertListCount != 1 || surface.vertInfo.vertsBlend)
return false;
const auto& vertList = surface.vertList[0];
#ifdef FEATURE_IW3
// IW3 has some models that are missing 1 (a single) tri in its first lod.
// It is not contained in any vert list or blend
// I think this is a bug (?), so omit anyway.
// The "one tri missing" is not supported by the exporter anyway.
if (vertList.boneOffset != 0 || vertList.triOffset != 0 || vertList.vertCount != surface.vertCount)
#else
if (vertList.boneOffset != 0 || vertList.triOffset != 0 || vertList.triCount != surface.triCount || vertList.vertCount != surface.vertCount)
#endif
return false;
}
return true;
}
bool HasDefaultArmatureForAllLods(const XModel& model)
{
for (auto lod = 0u; lod < model.numLods; lod++)
{
if (!HasDefaultArmatureForLod(model, lod))
return false;
}
return true;
}
void OmitDefaultArmature(XModelCommon& common)
{
common.m_bones.clear();
common.m_bone_weight_data.weights.clear();
common.m_vertex_bone_weights.resize(common.m_vertices.size());
for (auto& vertexWeights : common.m_vertex_bone_weights)
{
vertexWeights.weightOffset = 0u;
vertexWeights.weightCount = 0u;
}
}
void AddXModelBones(XModelCommon& out, const ZoneScriptStrings& scriptStrings, const XModel& model)
{
for (auto boneNum = 0u; boneNum < model.numBones; boneNum++)
{
XModelBone bone;
if (model.boneNames[boneNum] < scriptStrings.Count())
bone.name = scriptStrings[model.boneNames[boneNum]];
else
bone.name = "INVALID_BONE_NAME";
if (boneNum >= model.numRootBones)
bone.parentIndex = static_cast<int>(boneNum - static_cast<unsigned int>(model.parentList[boneNum - model.numRootBones]));
else
bone.parentIndex = std::nullopt;
bone.scale[0] = 1.0f;
bone.scale[1] = 1.0f;
bone.scale[2] = 1.0f;
const auto& baseMat = model.baseMat[boneNum];
bone.globalOffset[0] = baseMat.trans.x;
bone.globalOffset[1] = baseMat.trans.y;
bone.globalOffset[2] = baseMat.trans.z;
bone.globalRotation = {
.x = baseMat.quat.x,
.y = baseMat.quat.y,
.z = baseMat.quat.z,
.w = baseMat.quat.w,
};
if (boneNum < model.numRootBones)
{
bone.localOffset[0] = 0;
bone.localOffset[1] = 0;
bone.localOffset[2] = 0;
bone.localRotation = {.x = 0, .y = 0, .z = 0, .w = 1};
}
else
{
const auto* trans = &model.trans[(boneNum - model.numRootBones) * 3];
bone.localOffset[0] = trans[0];
bone.localOffset[1] = trans[1];
bone.localOffset[2] = trans[2];
const auto& quat = model.quats[boneNum - model.numRootBones];
bone.localRotation = {
.x = QuatInt16::ToFloat(quat.v[0]),
.y = QuatInt16::ToFloat(quat.v[1]),
.z = QuatInt16::ToFloat(quat.v[2]),
.w = QuatInt16::ToFloat(quat.v[3]),
};
}
out.m_bones.emplace_back(std::move(bone));
}
}
const char* AssetName(const char* input)
{
if (input && input[0] == ',')
return &input[1];
return input;
}
void AddXModelMaterials(XModelCommon& out, DistinctMapper<Material*>& materialMapper, const XModel& model)
{
for (auto surfaceMaterialNum = 0u; surfaceMaterialNum < model.numsurfs; surfaceMaterialNum++)
{
Material* material = model.materialHandles[surfaceMaterialNum];
if (materialMapper.Add(material))
{
XModelMaterial xMaterial;
xMaterial.ApplyDefaults();
xMaterial.name = AssetName(material->info.name);
const auto* colorMap = GetMaterialColorMap(material);
if (colorMap)
xMaterial.colorMapName = AssetName(colorMap->name);
const auto* normalMap = GetMaterialNormalMap(material);
if (normalMap)
xMaterial.normalMapName = AssetName(normalMap->name);
const auto* specularMap = GetMaterialSpecularMap(material);
if (specularMap)
xMaterial.specularMapName = AssetName(specularMap->name);
out.m_materials.emplace_back(std::move(xMaterial));
}
}
}
void AddXModelObjects(XModelCommon& out, const XModel& model, const unsigned lod, const DistinctMapper<Material*>& materialMapper)
{
const auto surfCount = model.lodInfo[lod].numsurfs;
const auto baseSurfaceIndex = model.lodInfo[lod].surfIndex;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
XModelObject object;
object.name = std::format("surf{}", surfIndex);
object.materialIndex = static_cast<int>(materialMapper.GetDistinctPositionByInputPosition(surfIndex + baseSurfaceIndex));
out.m_objects.emplace_back(std::move(object));
}
}
void AddXModelVertices(XModelCommon& out, const XModel& model, const unsigned lod)
{
XSurface* surfs;
unsigned surfCount;
if (!GetSurfaces(model, lod, surfs, surfCount))
return;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
const auto& surface = surfs[surfIndex];
for (auto vertexIndex = 0u; vertexIndex < surface.vertCount; vertexIndex++)
{
const auto& v = surface.verts0[vertexIndex];
XModelVertex vertex{};
vertex.coordinates[0] = v.xyz.x;
vertex.coordinates[1] = v.xyz.y;
vertex.coordinates[2] = v.xyz.z;
Common::Vec3UnpackUnitVec(v.normal, vertex.normal);
Common::Vec4UnpackGfxColor(v.color, vertex.color);
Common::Vec2UnpackTexCoords(v.texCoord, vertex.uv);
out.m_vertices.emplace_back(vertex);
}
}
}
void AllocateXModelBoneWeights(const XModel& model, const unsigned lod, XModelVertexBoneWeightCollection& weightCollection)
{
XSurface* surfs;
unsigned surfCount;
if (!GetSurfaces(model, lod, surfs, surfCount))
return;
auto totalWeightCount = 0u;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
const auto& surface = surfs[surfIndex];
if (surface.vertList)
{
totalWeightCount += surface.vertListCount;
}
if (surface.vertInfo.vertsBlend)
{
totalWeightCount += surface.vertInfo.vertCount[0] * 1;
totalWeightCount += surface.vertInfo.vertCount[1] * 2;
totalWeightCount += surface.vertInfo.vertCount[2] * 3;
totalWeightCount += surface.vertInfo.vertCount[3] * 4;
}
}
weightCollection.weights.resize(totalWeightCount);
}
float BoneWeight16(const uint16_t value)
{
return static_cast<float>(value) / static_cast<float>(std::numeric_limits<uint16_t>::max());
}
void AddXModelVertexBoneWeights(XModelCommon& out, const XModel& model, const unsigned lod)
{
XSurface* surfs;
unsigned surfCount;
if (!GetSurfaces(model, lod, surfs, surfCount))
return;
auto& weightCollection = out.m_bone_weight_data;
auto weightOffset = 0u;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
const auto& surface = surfs[surfIndex];
auto handledVertices = 0u;
if (surface.vertList)
{
#if defined(FEATURE_IW3) || defined(FEATURE_IW4)
assert(!surface.deformed);
#else
assert((surface.flags & XSURFACE_FLAG_DEFORMED) == 0);
#endif
for (auto vertListIndex = 0u; vertListIndex < surface.vertListCount; vertListIndex++)
{
const auto& vertList = surface.vertList[vertListIndex];
const auto boneWeightOffset = weightOffset;
weightCollection.weights[weightOffset++] =
XModelBoneWeight{.boneIndex = static_cast<unsigned>(vertList.boneOffset / sizeof(DObjSkelMat)), .weight = 1.0f};
for (auto vertListVertexOffset = 0u; vertListVertexOffset < vertList.vertCount; vertListVertexOffset++)
{
out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
}
handledVertices += vertList.vertCount;
}
}
auto vertsBlendOffset = 0u;
if (surface.vertInfo.vertsBlend)
{
#if defined(FEATURE_IW3) || defined(FEATURE_IW4)
assert(surface.deformed);
#else
assert((surface.flags & XSURFACE_FLAG_DEFORMED) > 0);
#endif
// 1 bone weight
for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[0]; vertIndex++)
{
const auto boneWeightOffset = weightOffset;
const unsigned boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex0, .weight = 1.0f};
vertsBlendOffset += 1;
out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
}
// 2 bone weights
for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[1]; vertIndex++)
{
const auto boneWeightOffset = weightOffset;
const unsigned boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
const unsigned boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
const auto boneWeight0 = 1.0f - boneWeight1;
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex0, .weight = boneWeight0};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex1, .weight = boneWeight1};
vertsBlendOffset += 3;
out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 2);
}
// 3 bone weights
for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[2]; vertIndex++)
{
const auto boneWeightOffset = weightOffset;
const unsigned boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
const unsigned boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
const unsigned boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2;
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex0, .weight = boneWeight0};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex1, .weight = boneWeight1};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex2, .weight = boneWeight2};
vertsBlendOffset += 5;
out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 3);
}
// 4 bone weights
for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[3]; vertIndex++)
{
const auto boneWeightOffset = weightOffset;
const unsigned boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
const unsigned boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
const unsigned boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
const unsigned boneIndex3 = surface.vertInfo.vertsBlend[vertsBlendOffset + 5] / sizeof(DObjSkelMat);
const auto boneWeight3 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 6]);
const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2 - boneWeight3;
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex0, .weight = boneWeight0};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex1, .weight = boneWeight1};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex2, .weight = boneWeight2};
weightCollection.weights[weightOffset++] = XModelBoneWeight{.boneIndex = boneIndex3, .weight = boneWeight3};
vertsBlendOffset += 7;
out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 4);
}
handledVertices +=
surface.vertInfo.vertCount[0] + surface.vertInfo.vertCount[1] + surface.vertInfo.vertCount[2] + surface.vertInfo.vertCount[3];
}
for (; handledVertices < surface.vertCount; handledVertices++)
{
out.m_vertex_bone_weights.emplace_back(0, 0);
}
}
}
void AddXModelFaces(XModelCommon& out, const XModel& model, const unsigned lod)
{
XSurface* surfs;
unsigned surfCount;
if (!GetSurfaces(model, lod, surfs, surfCount))
return;
for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
{
const auto& surface = surfs[surfIndex];
auto& object = out.m_objects[surfIndex];
object.m_faces.reserve(surface.triCount);
for (auto triIndex = 0u; triIndex < surface.triCount; triIndex++)
{
const auto& tri = surface.triIndices[triIndex];
XModelFace face{};
face.vertexIndex[0] = tri.i[0] + surface.baseVertIndex;
face.vertexIndex[1] = tri.i[1] + surface.baseVertIndex;
face.vertexIndex[2] = tri.i[2] + surface.baseVertIndex;
object.m_faces.emplace_back(face);
}
}
}
bool CanOmitDefaultArmature()
{
return ObjWriting::Configuration.ModelOutputFormat != ModelOutputFormat_e::XMODEL_EXPORT
&& ObjWriting::Configuration.ModelOutputFormat != ModelOutputFormat_e::XMODEL_BIN;
}
} // namespace
#set CLASS_NAME "ToCommonConverter" + GAME
namespace xmodel
{
std::optional<XModelCommon> CLASS_NAME::Convert(const XAssetInfoGeneric& assetInfo, const unsigned lod)
{
const auto& model = *reinterpret_cast<const XAssetInfo<XModel>*>(&assetInfo)->Asset();
if (lod >= model.numLods)
return std::nullopt;
XModelCommon result;
DistinctMapper<Material*> materialMapper(model.numsurfs);
AllocateXModelBoneWeights(model, lod, result.m_bone_weight_data);
result.m_name = std::format("{}_lod{}", model.name, lod);
AddXModelMaterials(result, materialMapper, model);
AddXModelObjects(result, model, lod, materialMapper);
AddXModelVertices(result, model, lod);
AddXModelFaces(result, model, lod);
// Keep armature handling consistent across all LODs so dumped GLTF/GLB round-trips
// preserve the same bone layout when re-imported.
if (!CanOmitDefaultArmature() || !HasDefaultArmatureForAllLods(model))
{
AddXModelBones(result, assetInfo.m_zone->m_script_strings, model);
AddXModelVertexBoneWeights(result, model, lod);
}
else
{
OmitDefaultArmature(result);
}
return result;
}
}
@@ -0,0 +1,24 @@
#pragma once
#include "Pool/XAssetInfo.h"
#include "XModel/XModelCommon.h"
#include <optional>
namespace xmodel
{
class ToCommonConverter
{
public:
ToCommonConverter() = default;
virtual ~ToCommonConverter() = default;
ToCommonConverter(const ToCommonConverter& other) = default;
ToCommonConverter(ToCommonConverter&& other) noexcept = default;
ToCommonConverter& operator=(const ToCommonConverter& other) = default;
ToCommonConverter& operator=(ToCommonConverter&& other) noexcept = default;
static ToCommonConverter* GetForGame(GameId gameId);
virtual std::optional<XModelCommon> Convert(const XAssetInfoGeneric& assetInfo, unsigned lod) = 0;
};
} // namespace xmodel
@@ -0,0 +1,25 @@
#options GAME (IW3, IW4, IW5, T5, T6)
#filename "Game/" + GAME + "/XModel/XModelToCommonConverter" + GAME + ".h"
#set GAME_HEADER "\"Game/" + GAME + "/" + GAME + ".h\""
// This file was templated.
// See XModelToCommonConverter.h.template.
// Do not modify, changes will be lost.
#pragma once
#include "XModel/XModelToCommonConverter.h"
#include GAME_HEADER
#set CLASS_NAME "ToCommonConverter" + GAME
namespace xmodel
{
class CLASS_NAME final : public ToCommonConverter
{
public:
std::optional<XModelCommon> Convert(const XAssetInfoGeneric& assetInfo, unsigned lod) override;
};
} // namespace xmodel