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:
Generated
+117
-7
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}>();
|
||||
|
||||
@@ -17,6 +17,9 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
cors: false,
|
||||
},
|
||||
publicDir: fileURLToPath(new URL("./public", import.meta.url)),
|
||||
plugins: [
|
||||
vue(),
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
Vendored
+1
-1
Submodule thirdparty/webwindowed updated: 1e5e89d054...afc7a4f938
Reference in New Issue
Block a user