mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-01-26 07:45:33 +01:00
VideoCommon: add material asset. A material is similar to other graphics engines where it provides data to be used in conjunction with a shader asset to generate a runtime AbstractShader
This commit is contained in:
parent
7bb04ff1dc
commit
77511e8e7c
@ -637,6 +637,7 @@
|
|||||||
<ClInclude Include="VideoCommon\Assets\CustomAssetLoader.h" />
|
<ClInclude Include="VideoCommon\Assets\CustomAssetLoader.h" />
|
||||||
<ClInclude Include="VideoCommon\Assets\CustomTextureData.h" />
|
<ClInclude Include="VideoCommon\Assets\CustomTextureData.h" />
|
||||||
<ClInclude Include="VideoCommon\Assets\DirectFilesystemAssetLibrary.h" />
|
<ClInclude Include="VideoCommon\Assets\DirectFilesystemAssetLibrary.h" />
|
||||||
|
<ClInclude Include="VideoCommon\Assets\MaterialAsset.h" />
|
||||||
<ClInclude Include="VideoCommon\Assets\ShaderAsset.h" />
|
<ClInclude Include="VideoCommon\Assets\ShaderAsset.h" />
|
||||||
<ClInclude Include="VideoCommon\Assets\TextureAsset.h" />
|
<ClInclude Include="VideoCommon\Assets\TextureAsset.h" />
|
||||||
<ClInclude Include="VideoCommon\AsyncRequests.h" />
|
<ClInclude Include="VideoCommon\AsyncRequests.h" />
|
||||||
@ -1253,6 +1254,7 @@
|
|||||||
<ClCompile Include="VideoCommon\Assets\CustomAssetLoader.cpp" />
|
<ClCompile Include="VideoCommon\Assets\CustomAssetLoader.cpp" />
|
||||||
<ClCompile Include="VideoCommon\Assets\CustomTextureData.cpp" />
|
<ClCompile Include="VideoCommon\Assets\CustomTextureData.cpp" />
|
||||||
<ClCompile Include="VideoCommon\Assets\DirectFilesystemAssetLibrary.cpp" />
|
<ClCompile Include="VideoCommon\Assets\DirectFilesystemAssetLibrary.cpp" />
|
||||||
|
<ClCompile Include="VideoCommon\Assets\MaterialAsset.cpp" />
|
||||||
<ClCompile Include="VideoCommon\Assets\ShaderAsset.cpp" />
|
<ClCompile Include="VideoCommon\Assets\ShaderAsset.cpp" />
|
||||||
<ClCompile Include="VideoCommon\Assets\TextureAsset.cpp" />
|
<ClCompile Include="VideoCommon\Assets\TextureAsset.cpp" />
|
||||||
<ClCompile Include="VideoCommon\AsyncRequests.cpp" />
|
<ClCompile Include="VideoCommon\AsyncRequests.cpp" />
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
namespace VideoCommon
|
namespace VideoCommon
|
||||||
{
|
{
|
||||||
class CustomTextureData;
|
class CustomTextureData;
|
||||||
|
struct MaterialData;
|
||||||
struct PixelShaderData;
|
struct PixelShaderData;
|
||||||
|
|
||||||
// This class provides functionality to load
|
// This class provides functionality to load
|
||||||
@ -49,5 +50,8 @@ public:
|
|||||||
|
|
||||||
// Loads a pixel shader
|
// Loads a pixel shader
|
||||||
virtual LoadInfo LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) = 0;
|
virtual LoadInfo LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) = 0;
|
||||||
|
|
||||||
|
// Loads a material
|
||||||
|
virtual LoadInfo LoadMaterial(const AssetID& asset_id, MaterialData* data) = 0;
|
||||||
};
|
};
|
||||||
} // namespace VideoCommon
|
} // namespace VideoCommon
|
||||||
|
@ -97,4 +97,11 @@ CustomAssetLoader::LoadPixelShader(const CustomAssetLibrary::AssetID& asset_id,
|
|||||||
{
|
{
|
||||||
return LoadOrCreateAsset<PixelShaderAsset>(asset_id, m_pixel_shaders, std::move(library));
|
return LoadOrCreateAsset<PixelShaderAsset>(asset_id, m_pixel_shaders, std::move(library));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<MaterialAsset>
|
||||||
|
CustomAssetLoader::LoadMaterial(const CustomAssetLibrary::AssetID& asset_id,
|
||||||
|
std::shared_ptr<CustomAssetLibrary> library)
|
||||||
|
{
|
||||||
|
return LoadOrCreateAsset<MaterialAsset>(asset_id, m_materials, std::move(library));
|
||||||
|
}
|
||||||
} // namespace VideoCommon
|
} // namespace VideoCommon
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
#include "Common/Flag.h"
|
#include "Common/Flag.h"
|
||||||
#include "Common/WorkQueueThread.h"
|
#include "Common/WorkQueueThread.h"
|
||||||
#include "VideoCommon/Assets/CustomAsset.h"
|
#include "VideoCommon/Assets/CustomAsset.h"
|
||||||
|
#include "VideoCommon/Assets/MaterialAsset.h"
|
||||||
#include "VideoCommon/Assets/ShaderAsset.h"
|
#include "VideoCommon/Assets/ShaderAsset.h"
|
||||||
#include "VideoCommon/Assets/TextureAsset.h"
|
#include "VideoCommon/Assets/TextureAsset.h"
|
||||||
|
|
||||||
@ -46,6 +47,9 @@ public:
|
|||||||
std::shared_ptr<PixelShaderAsset> LoadPixelShader(const CustomAssetLibrary::AssetID& asset_id,
|
std::shared_ptr<PixelShaderAsset> LoadPixelShader(const CustomAssetLibrary::AssetID& asset_id,
|
||||||
std::shared_ptr<CustomAssetLibrary> library);
|
std::shared_ptr<CustomAssetLibrary> library);
|
||||||
|
|
||||||
|
std::shared_ptr<MaterialAsset> LoadMaterial(const CustomAssetLibrary::AssetID& asset_id,
|
||||||
|
std::shared_ptr<CustomAssetLibrary> library);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// TODO C++20: use a 'derived_from' concept against 'CustomAsset' when available
|
// TODO C++20: use a 'derived_from' concept against 'CustomAsset' when available
|
||||||
template <typename AssetType>
|
template <typename AssetType>
|
||||||
@ -79,6 +83,7 @@ private:
|
|||||||
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<RawTextureAsset>> m_textures;
|
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<RawTextureAsset>> m_textures;
|
||||||
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<GameTextureAsset>> m_game_textures;
|
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<GameTextureAsset>> m_game_textures;
|
||||||
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<PixelShaderAsset>> m_pixel_shaders;
|
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<PixelShaderAsset>> m_pixel_shaders;
|
||||||
|
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<MaterialAsset>> m_materials;
|
||||||
std::thread m_asset_monitor_thread;
|
std::thread m_asset_monitor_thread;
|
||||||
Common::Flag m_asset_monitor_thread_shutdown;
|
Common::Flag m_asset_monitor_thread_shutdown;
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
#include "Common/Logging/Log.h"
|
#include "Common/Logging/Log.h"
|
||||||
#include "Common/StringUtil.h"
|
#include "Common/StringUtil.h"
|
||||||
#include "VideoCommon/Assets/CustomTextureData.h"
|
#include "VideoCommon/Assets/CustomTextureData.h"
|
||||||
|
#include "VideoCommon/Assets/MaterialAsset.h"
|
||||||
#include "VideoCommon/Assets/ShaderAsset.h"
|
#include "VideoCommon/Assets/ShaderAsset.h"
|
||||||
|
|
||||||
namespace VideoCommon
|
namespace VideoCommon
|
||||||
@ -144,6 +145,61 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadPixelShader(const
|
|||||||
return LoadInfo{approx_mem_size, GetLastAssetWriteTime(asset_id)};
|
return LoadInfo{approx_mem_size, GetLastAssetWriteTime(asset_id)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMaterial(const AssetID& asset_id,
|
||||||
|
MaterialData* data)
|
||||||
|
{
|
||||||
|
const auto asset_map = GetAssetMapForID(asset_id);
|
||||||
|
|
||||||
|
// Material is expected to have one asset mapped
|
||||||
|
if (asset_map.empty() || asset_map.size() > 1)
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - material expected to have one file mapped!", asset_id);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto& asset_path = asset_map.begin()->second;
|
||||||
|
|
||||||
|
std::string json_data;
|
||||||
|
if (!File::ReadFileToString(asset_path.string(), json_data))
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - material failed to load the json file '{}',",
|
||||||
|
asset_id, asset_path.string());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
picojson::value root;
|
||||||
|
const auto error = picojson::parse(root, json_data);
|
||||||
|
|
||||||
|
if (!error.empty())
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(
|
||||||
|
VIDEO,
|
||||||
|
"Asset '{}' error - material failed to load the json file '{}', due to parse error: {}",
|
||||||
|
asset_id, asset_path.string(), error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!root.is<picojson::object>())
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO,
|
||||||
|
"Asset '{}' error - material failed to load the json file '{}', due to root not "
|
||||||
|
"being an object!",
|
||||||
|
asset_id, asset_path.string());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& root_obj = root.get<picojson::object>();
|
||||||
|
|
||||||
|
if (!MaterialData::FromJson(asset_id, root_obj, data))
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO,
|
||||||
|
"Asset '{}' error - material failed to load the json file '{}', as material "
|
||||||
|
"json could not be parsed!",
|
||||||
|
asset_id, asset_path.string());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoadInfo{json_data.size(), GetLastAssetWriteTime(asset_id)};
|
||||||
|
}
|
||||||
|
|
||||||
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const AssetID& asset_id,
|
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const AssetID& asset_id,
|
||||||
CustomTextureData* data)
|
CustomTextureData* data)
|
||||||
{
|
{
|
||||||
|
@ -23,6 +23,7 @@ public:
|
|||||||
|
|
||||||
LoadInfo LoadTexture(const AssetID& asset_id, CustomTextureData* data) override;
|
LoadInfo LoadTexture(const AssetID& asset_id, CustomTextureData* data) override;
|
||||||
LoadInfo LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) override;
|
LoadInfo LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) override;
|
||||||
|
LoadInfo LoadMaterial(const AssetID& asset_id, MaterialData* data) override;
|
||||||
|
|
||||||
// Gets the latest time from amongst all the files in the asset map
|
// Gets the latest time from amongst all the files in the asset map
|
||||||
TimeType GetLastAssetWriteTime(const AssetID& asset_id) const override;
|
TimeType GetLastAssetWriteTime(const AssetID& asset_id) const override;
|
||||||
|
166
Source/Core/VideoCommon/Assets/MaterialAsset.cpp
Normal file
166
Source/Core/VideoCommon/Assets/MaterialAsset.cpp
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "VideoCommon/Assets/MaterialAsset.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Common/Logging/Log.h"
|
||||||
|
#include "Common/StringUtil.h"
|
||||||
|
#include "VideoCommon/Assets/CustomAssetLibrary.h"
|
||||||
|
|
||||||
|
namespace VideoCommon
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool ParsePropertyValue(const CustomAssetLibrary::AssetID& asset_id, MaterialProperty::Type type,
|
||||||
|
const picojson::value& json_value,
|
||||||
|
std::optional<MaterialProperty::Value>* value)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case MaterialProperty::Type::Type_TextureAsset:
|
||||||
|
{
|
||||||
|
if (json_value.is<std::string>())
|
||||||
|
{
|
||||||
|
*value = json_value.to_str();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse the json, value is not valid for type '{}'",
|
||||||
|
asset_id, type);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ParseMaterialProperties(const CustomAssetLibrary::AssetID& asset_id,
|
||||||
|
const picojson::array& values_data,
|
||||||
|
std::vector<MaterialProperty>* material_property)
|
||||||
|
{
|
||||||
|
for (const auto& value_data : values_data)
|
||||||
|
{
|
||||||
|
VideoCommon::MaterialProperty property;
|
||||||
|
if (!value_data.is<picojson::object>())
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse the json, value is not the right json type",
|
||||||
|
asset_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& value_data_obj = value_data.get<picojson::object>();
|
||||||
|
|
||||||
|
const auto type_iter = value_data_obj.find("type");
|
||||||
|
if (type_iter == value_data_obj.end())
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse the json, value entry 'type' not found",
|
||||||
|
asset_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!type_iter->second.is<std::string>())
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO,
|
||||||
|
"Asset '{}' failed to parse the json, value entry 'type' is not "
|
||||||
|
"the right json type",
|
||||||
|
asset_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string type = type_iter->second.to_str();
|
||||||
|
Common::ToLower(&type);
|
||||||
|
if (type == "texture_asset")
|
||||||
|
{
|
||||||
|
property.m_type = MaterialProperty::Type::Type_TextureAsset;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO,
|
||||||
|
"Asset '{}' failed to parse the json, value entry 'type' is "
|
||||||
|
"an invalid option",
|
||||||
|
asset_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto code_name_iter = value_data_obj.find("code_name");
|
||||||
|
if (code_name_iter == value_data_obj.end())
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO,
|
||||||
|
"Asset '{}' failed to parse the json, value entry "
|
||||||
|
"'code_name' not found",
|
||||||
|
asset_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!code_name_iter->second.is<std::string>())
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO,
|
||||||
|
"Asset '{}' failed to parse the json, value entry 'code_name' is not "
|
||||||
|
"the right json type",
|
||||||
|
asset_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
property.m_code_name = code_name_iter->second.to_str();
|
||||||
|
|
||||||
|
const auto value_iter = value_data_obj.find("value");
|
||||||
|
if (value_iter != value_data_obj.end())
|
||||||
|
{
|
||||||
|
if (!ParsePropertyValue(asset_id, property.m_type, value_iter->second, &property.m_value))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
material_property->push_back(std::move(property));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
bool MaterialData::FromJson(const CustomAssetLibrary::AssetID& asset_id,
|
||||||
|
const picojson::object& json, MaterialData* data)
|
||||||
|
{
|
||||||
|
const auto values_iter = json.find("values");
|
||||||
|
if (values_iter == json.end())
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, 'values' not found", asset_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!values_iter->second.is<picojson::array>())
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, 'values' is not the right json type",
|
||||||
|
asset_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& values_array = values_iter->second.get<picojson::array>();
|
||||||
|
|
||||||
|
if (!ParseMaterialProperties(asset_id, values_array, &data->properties))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const auto shader_asset_iter = json.find("shader_asset");
|
||||||
|
if (shader_asset_iter == json.end())
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, 'shader_asset' not found", asset_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!shader_asset_iter->second.is<std::string>())
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(VIDEO,
|
||||||
|
"Asset '{}' failed to parse json, 'shader_asset' is not the right json type",
|
||||||
|
asset_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
data->shader_asset = shader_asset_iter->second.to_str();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomAssetLibrary::LoadInfo MaterialAsset::LoadImpl(const CustomAssetLibrary::AssetID& asset_id)
|
||||||
|
{
|
||||||
|
auto potential_data = std::make_shared<MaterialData>();
|
||||||
|
const auto loaded_info = m_owning_library->LoadMaterial(asset_id, potential_data.get());
|
||||||
|
if (loaded_info.m_bytes_loaded == 0)
|
||||||
|
return {};
|
||||||
|
{
|
||||||
|
std::lock_guard lk(m_data_lock);
|
||||||
|
m_loaded = true;
|
||||||
|
m_data = std::move(potential_data);
|
||||||
|
}
|
||||||
|
return loaded_info;
|
||||||
|
}
|
||||||
|
} // namespace VideoCommon
|
61
Source/Core/VideoCommon/Assets/MaterialAsset.h
Normal file
61
Source/Core/VideoCommon/Assets/MaterialAsset.h
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <picojson.h>
|
||||||
|
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
#include "Common/EnumFormatter.h"
|
||||||
|
#include "VideoCommon/Assets/CustomAsset.h"
|
||||||
|
|
||||||
|
namespace VideoCommon
|
||||||
|
{
|
||||||
|
struct MaterialProperty
|
||||||
|
{
|
||||||
|
using Value = std::variant<std::string>;
|
||||||
|
enum class Type
|
||||||
|
{
|
||||||
|
Type_Undefined,
|
||||||
|
Type_TextureAsset,
|
||||||
|
Type_Max = Type_TextureAsset
|
||||||
|
};
|
||||||
|
std::string m_code_name;
|
||||||
|
Type m_type = Type::Type_Undefined;
|
||||||
|
std::optional<Value> m_value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MaterialData
|
||||||
|
{
|
||||||
|
static bool FromJson(const CustomAssetLibrary::AssetID& asset_id, const picojson::object& json,
|
||||||
|
MaterialData* data);
|
||||||
|
std::string shader_asset;
|
||||||
|
std::vector<MaterialProperty> properties;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Much like Unity and Unreal materials, a Dolphin material does very little on its own
|
||||||
|
// Its sole purpose is to provide data (through properties) that are used in conjunction
|
||||||
|
// with a shader asset that is provided by name. It is up to user of this asset to
|
||||||
|
// use the two together to create the relevant runtime data
|
||||||
|
class MaterialAsset final : public CustomLoadableAsset<MaterialData>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using CustomLoadableAsset::CustomLoadableAsset;
|
||||||
|
|
||||||
|
private:
|
||||||
|
CustomAssetLibrary::LoadInfo LoadImpl(const CustomAssetLibrary::AssetID& asset_id) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace VideoCommon
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct fmt::formatter<VideoCommon::MaterialProperty::Type>
|
||||||
|
: EnumFormatter<VideoCommon::MaterialProperty::Type::Type_Max>
|
||||||
|
{
|
||||||
|
constexpr formatter() : EnumFormatter({"Undefined", "Texture"}) {}
|
||||||
|
};
|
@ -18,6 +18,8 @@ add_library(videocommon
|
|||||||
Assets/CustomTextureData.h
|
Assets/CustomTextureData.h
|
||||||
Assets/DirectFilesystemAssetLibrary.cpp
|
Assets/DirectFilesystemAssetLibrary.cpp
|
||||||
Assets/DirectFilesystemAssetLibrary.h
|
Assets/DirectFilesystemAssetLibrary.h
|
||||||
|
Assets/MaterialAsset.cpp
|
||||||
|
Assets/MaterialAsset.h
|
||||||
Assets/ShaderAsset.cpp
|
Assets/ShaderAsset.cpp
|
||||||
Assets/ShaderAsset.h
|
Assets/ShaderAsset.h
|
||||||
Assets/TextureAsset.cpp
|
Assets/TextureAsset.cpp
|
||||||
|
Loading…
x
Reference in New Issue
Block a user