feat: Add Camera component / Add cooked model and texture loading
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
#include <destrum/Assets/AssetManager.h>
|
||||
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <destrum/Assets/ModelFile.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
|
||||
namespace {
|
||||
@@ -11,6 +13,34 @@ namespace {
|
||||
}
|
||||
return std::string{reference};
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> FindCookedModel(const std::string& reference) {
|
||||
if (reference.find("://") == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
try {
|
||||
const std::filesystem::path cookedPath = AssetFS::GetInstance().GetCookedPathForFile(reference);
|
||||
if (cookedPath.extension() == ModelFile::Extension) {
|
||||
return cookedPath;
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::warn("Importing {} instead of using its cooked model: {}", reference, exception.what());
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Cooked texture paths are relative to the model file, imported ones are absolute
|
||||
void ResolveTexturePaths(ModelDoc::ModelAsset& model, const std::filesystem::path& modelDir) {
|
||||
for (ModelDoc::MaterialInfo& material : model.materials) {
|
||||
for (ModelDoc::TextureInfo& texture : material.textures) {
|
||||
if (texture.embedded || texture.path.empty() || std::filesystem::path{texture.path}.is_absolute()) {
|
||||
continue;
|
||||
}
|
||||
texture.path = (modelDir / texture.path).lexically_normal().generic_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path AssetManager::Resolve(std::string_view reference) const {
|
||||
@@ -32,9 +62,7 @@ ModelDoc::ModelAsset AssetManager::LoadModel(
|
||||
const ModelDoc::LoadOptions& options) const
|
||||
{
|
||||
const std::string sourceReference = ToReference(reference);
|
||||
ModelDoc::ModelAsset model = ModelDoc::LoadModel(
|
||||
Resolve(sourceReference).generic_string(),
|
||||
options);
|
||||
ModelDoc::ModelAsset model = LoadCookedOrImport(sourceReference, options);
|
||||
|
||||
model.sourcePath = sourceReference;
|
||||
model.skeleton.assetReference.path = sourceReference;
|
||||
@@ -44,6 +72,9 @@ ModelDoc::ModelAsset AssetManager::LoadModel(
|
||||
primitive.mesh.assetReference.subresource = primitive.sourceMeshIndex != ModelDoc::InvalidIndex
|
||||
? "mesh:" + std::to_string(primitive.sourceMeshIndex)
|
||||
: "primitive:" + std::to_string(index);
|
||||
if (primitive.mesh.skeleton) {
|
||||
primitive.mesh.skeleton->assetReference.path = sourceReference;
|
||||
}
|
||||
}
|
||||
for (std::size_t index = 0; index < model.materials.size(); ++index) {
|
||||
auto& material = model.materials[index];
|
||||
@@ -59,6 +90,24 @@ ModelDoc::ModelAsset AssetManager::LoadModel(
|
||||
return model;
|
||||
}
|
||||
|
||||
ModelDoc::ModelAsset AssetManager::LoadCookedOrImport(
|
||||
const std::string& reference,
|
||||
const ModelDoc::LoadOptions& options) const
|
||||
{
|
||||
if (const auto cookedPath = FindCookedModel(reference)) {
|
||||
ModelDoc::ModelAsset model = ModelFile::ReadFromFile(*cookedPath);
|
||||
if (ModelFile::SupportsLoadOptions(model, options)) {
|
||||
ModelFile::ApplyLoadOptions(model, options);
|
||||
ResolveTexturePaths(model, cookedPath->parent_path());
|
||||
spdlog::debug("Loaded cooked model {}", reference);
|
||||
return model;
|
||||
}
|
||||
spdlog::debug("Importing {}, its cooked model doesn't match the requested load options", reference);
|
||||
}
|
||||
|
||||
return ModelDoc::LoadModel(Resolve(reference).generic_string(), options);
|
||||
}
|
||||
|
||||
std::vector<SkeletalAnimation> AssetManager::LoadAnimationClips(
|
||||
std::string_view reference,
|
||||
const Skeleton& targetSkeleton) const
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
#ifndef DESTRUM_BINARYSTREAM_H
|
||||
#define DESTRUM_BINARYSTREAM_H
|
||||
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
namespace AssetBinary {
|
||||
static_assert(std::endian::native == std::endian::little, "Cooked assets are stored little endian");
|
||||
|
||||
using Magic = std::array<char, 4>;
|
||||
|
||||
template <typename T>
|
||||
concept RawCopyable = std::is_trivially_copyable_v<T>;
|
||||
|
||||
class BinaryWriter {
|
||||
public:
|
||||
template <RawCopyable T>
|
||||
void Write(const T& value) {
|
||||
const auto* bytes = reinterpret_cast<const std::uint8_t*>(&value);
|
||||
m_Bytes.insert(m_Bytes.end(), bytes, bytes + sizeof(T));
|
||||
}
|
||||
|
||||
template <RawCopyable T>
|
||||
void WriteArray(const std::vector<T>& values) {
|
||||
WriteCount(values.size());
|
||||
const auto* bytes = reinterpret_cast<const std::uint8_t*>(values.data());
|
||||
m_Bytes.insert(m_Bytes.end(), bytes, bytes + values.size() * sizeof(T));
|
||||
}
|
||||
|
||||
void WriteCount(std::size_t count) {
|
||||
Write(static_cast<std::uint64_t>(count));
|
||||
}
|
||||
|
||||
void WriteBool(bool value) {
|
||||
Write(static_cast<std::uint8_t>(value ? 1 : 0));
|
||||
}
|
||||
|
||||
void WriteString(std::string_view value) {
|
||||
WriteCount(value.size());
|
||||
m_Bytes.insert(m_Bytes.end(), value.begin(), value.end());
|
||||
}
|
||||
|
||||
void WriteStrings(const std::vector<std::string>& values) {
|
||||
WriteCount(values.size());
|
||||
for (const std::string& value : values) {
|
||||
WriteString(value);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteHeader(const Magic& magic, std::uint32_t version) {
|
||||
Write(magic);
|
||||
Write(version);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<std::uint8_t> TakeBytes() {
|
||||
return std::move(m_Bytes);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::uint8_t> m_Bytes;
|
||||
};
|
||||
|
||||
class BinaryReader {
|
||||
public:
|
||||
explicit BinaryReader(std::span<const std::uint8_t> bytes)
|
||||
: m_Bytes(bytes) {
|
||||
}
|
||||
|
||||
template <RawCopyable T>
|
||||
[[nodiscard]] T Read() {
|
||||
T value{};
|
||||
std::memcpy(&value, Take(sizeof(T)).data(), sizeof(T));
|
||||
return value;
|
||||
}
|
||||
|
||||
template <RawCopyable T>
|
||||
[[nodiscard]] std::vector<T> ReadArray() {
|
||||
const std::size_t count = ReadCount(sizeof(T));
|
||||
std::vector<T> values(count);
|
||||
if (count > 0) {
|
||||
std::memcpy(values.data(), Take(count * sizeof(T)).data(), count * sizeof(T));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t ReadCount(std::size_t minElementSize) {
|
||||
const auto count = Read<std::uint64_t>();
|
||||
if (count > Remaining() / minElementSize) {
|
||||
throw std::runtime_error("Cooked asset file is corrupt: element count is out of range");
|
||||
}
|
||||
return static_cast<std::size_t>(count);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ReadBool() {
|
||||
return Read<std::uint8_t>() != 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string ReadString() {
|
||||
const std::size_t length = ReadCount(1);
|
||||
const auto bytes = Take(length);
|
||||
return std::string(bytes.begin(), bytes.end());
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<std::string> ReadStrings() {
|
||||
std::vector<std::string> values(ReadCount(sizeof(std::uint64_t)));
|
||||
for (std::string& value : values) {
|
||||
value = ReadString();
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
void ReadHeader(const Magic& magic, std::uint32_t version, std::string_view kind) {
|
||||
if (Read<Magic>() != magic) {
|
||||
throw std::runtime_error("Not a cooked " + std::string{kind} + " file");
|
||||
}
|
||||
|
||||
const auto fileVersion = Read<std::uint32_t>();
|
||||
if (fileVersion != version) {
|
||||
throw std::runtime_error(
|
||||
"Cooked " + std::string{kind} + " file has version " + std::to_string(fileVersion) +
|
||||
", expected " + std::to_string(version) + " (recook the asset)");
|
||||
}
|
||||
}
|
||||
|
||||
void ExpectEnd(std::string_view kind) const {
|
||||
if (m_Offset != m_Bytes.size()) {
|
||||
throw std::runtime_error("Cooked " + std::string{kind} + " file has unexpected data at the end");
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::size_t Remaining() const {
|
||||
return m_Bytes.size() - m_Offset;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const std::uint8_t> Take(std::size_t size) {
|
||||
if (size > Remaining()) {
|
||||
throw std::runtime_error("Cooked asset file is truncated");
|
||||
}
|
||||
const auto bytes = m_Bytes.subspan(m_Offset, size);
|
||||
m_Offset += size;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::span<const std::uint8_t> m_Bytes;
|
||||
std::size_t m_Offset{0};
|
||||
};
|
||||
|
||||
inline void WriteAllBytes(const std::filesystem::path& path, const std::vector<std::uint8_t>& bytes) {
|
||||
std::ofstream file(path, std::ios::binary | std::ios::trunc);
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to open file for writing: " + path.string());
|
||||
}
|
||||
file.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to write file: " + path.string());
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::vector<std::uint8_t> ReadAllBytes(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to open file: " + path.string());
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(file.tellg()));
|
||||
file.seekg(0);
|
||||
file.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to read file: " + path.string());
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline std::optional<std::uint32_t> ReadVersion(const std::filesystem::path& path, const Magic& magic) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::array<std::uint8_t, sizeof(Magic) + sizeof(std::uint32_t)> header{};
|
||||
file.read(reinterpret_cast<char*>(header.data()), static_cast<std::streamsize>(header.size()));
|
||||
if (!file) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
BinaryReader reader{header};
|
||||
if (reader.Read<Magic>() != magic) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return reader.Read<std::uint32_t>();
|
||||
}
|
||||
}
|
||||
|
||||
#endif // DESTRUM_BINARYSTREAM_H
|
||||
@@ -1,141 +1,18 @@
|
||||
#include <destrum/Assets/ModelFile.h>
|
||||
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include "BinaryStream.h"
|
||||
|
||||
namespace {
|
||||
static_assert(std::endian::native == std::endian::little, "Model files are stored little endian");
|
||||
using AssetBinary::BinaryReader;
|
||||
using AssetBinary::BinaryWriter;
|
||||
|
||||
static_assert(sizeof(CPUMesh::Vertex) == 48);
|
||||
static_assert(sizeof(CPUMesh::SkinningData) == 32);
|
||||
static_assert(sizeof(SkeletalAnimation::Keyframe) == 44);
|
||||
static_assert(sizeof(glm::mat4) == 64);
|
||||
|
||||
constexpr std::array<char, 4> Magic{'D', 'M', 'D', 'L'};
|
||||
|
||||
template <typename T>
|
||||
concept RawCopyable = std::is_trivially_copyable_v<T>;
|
||||
|
||||
class BinaryWriter {
|
||||
public:
|
||||
template <RawCopyable T>
|
||||
void Write(const T& value) {
|
||||
const auto* bytes = reinterpret_cast<const std::uint8_t*>(&value);
|
||||
m_Bytes.insert(m_Bytes.end(), bytes, bytes + sizeof(T));
|
||||
}
|
||||
|
||||
template <RawCopyable T>
|
||||
void WriteArray(const std::vector<T>& values) {
|
||||
WriteCount(values.size());
|
||||
const auto* bytes = reinterpret_cast<const std::uint8_t*>(values.data());
|
||||
m_Bytes.insert(m_Bytes.end(), bytes, bytes + values.size() * sizeof(T));
|
||||
}
|
||||
|
||||
void WriteCount(std::size_t count) {
|
||||
Write(static_cast<std::uint64_t>(count));
|
||||
}
|
||||
|
||||
void WriteBool(bool value) {
|
||||
Write(static_cast<std::uint8_t>(value ? 1 : 0));
|
||||
}
|
||||
|
||||
void WriteString(std::string_view value) {
|
||||
WriteCount(value.size());
|
||||
m_Bytes.insert(m_Bytes.end(), value.begin(), value.end());
|
||||
}
|
||||
|
||||
void WriteStrings(const std::vector<std::string>& values) {
|
||||
WriteCount(values.size());
|
||||
for (const std::string& value : values) {
|
||||
WriteString(value);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<std::uint8_t> TakeBytes() {
|
||||
return std::move(m_Bytes);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::uint8_t> m_Bytes;
|
||||
};
|
||||
|
||||
class BinaryReader {
|
||||
public:
|
||||
explicit BinaryReader(std::span<const std::uint8_t> bytes)
|
||||
: m_Bytes(bytes) {
|
||||
}
|
||||
|
||||
template <RawCopyable T>
|
||||
[[nodiscard]] T Read() {
|
||||
T value{};
|
||||
std::memcpy(&value, Take(sizeof(T)).data(), sizeof(T));
|
||||
return value;
|
||||
}
|
||||
|
||||
template <RawCopyable T>
|
||||
[[nodiscard]] std::vector<T> ReadArray() {
|
||||
const std::size_t count = ReadCount(sizeof(T));
|
||||
std::vector<T> values(count);
|
||||
if (count > 0) {
|
||||
std::memcpy(values.data(), Take(count * sizeof(T)).data(), count * sizeof(T));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
// Every element takes at least minElementSize bytes, which keeps a corrupt count
|
||||
// from allocating far more than the file could ever contain
|
||||
[[nodiscard]] std::size_t ReadCount(std::size_t minElementSize) {
|
||||
const auto count = Read<std::uint64_t>();
|
||||
if (count > Remaining() / minElementSize) {
|
||||
throw std::runtime_error("Model file is corrupt: element count is out of range");
|
||||
}
|
||||
return static_cast<std::size_t>(count);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ReadBool() {
|
||||
return Read<std::uint8_t>() != 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string ReadString() {
|
||||
const std::size_t length = ReadCount(1);
|
||||
const auto bytes = Take(length);
|
||||
return std::string(bytes.begin(), bytes.end());
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<std::string> ReadStrings() {
|
||||
std::vector<std::string> values(ReadCount(sizeof(std::uint64_t)));
|
||||
for (std::string& value : values) {
|
||||
value = ReadString();
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool AtEnd() const {
|
||||
return m_Offset == m_Bytes.size();
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::size_t Remaining() const {
|
||||
return m_Bytes.size() - m_Offset;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const std::uint8_t> Take(std::size_t size) {
|
||||
if (size > Remaining()) {
|
||||
throw std::runtime_error("Model file is truncated");
|
||||
}
|
||||
const auto bytes = m_Bytes.subspan(m_Offset, size);
|
||||
m_Offset += size;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::span<const std::uint8_t> m_Bytes;
|
||||
std::size_t m_Offset{0};
|
||||
};
|
||||
constexpr AssetBinary::Magic Magic{'D', 'M', 'D', 'L'};
|
||||
constexpr std::string_view Kind = "model";
|
||||
|
||||
void WriteAssetReference(BinaryWriter& writer, const AssetReference& reference) {
|
||||
writer.WriteString(reference.path);
|
||||
@@ -160,7 +37,6 @@ namespace {
|
||||
|
||||
writer.WriteArray(skeleton.inverseBindMatrices);
|
||||
|
||||
// Joint has padding after its id, so it's written field by field
|
||||
writer.WriteCount(skeleton.joints.size());
|
||||
for (const Joint& joint : skeleton.joints) {
|
||||
writer.Write(joint.id);
|
||||
@@ -350,8 +226,7 @@ namespace {
|
||||
|
||||
std::vector<std::uint8_t> ModelFile::Write(const ModelDoc::ModelAsset& model) {
|
||||
BinaryWriter writer;
|
||||
writer.Write(Magic);
|
||||
writer.Write(Version);
|
||||
writer.WriteHeader(Magic, Version);
|
||||
|
||||
writer.WriteString(model.sourcePath);
|
||||
|
||||
@@ -377,17 +252,7 @@ std::vector<std::uint8_t> ModelFile::Write(const ModelDoc::ModelAsset& model) {
|
||||
|
||||
ModelDoc::ModelAsset ModelFile::Read(std::span<const std::uint8_t> bytes) {
|
||||
BinaryReader reader{bytes};
|
||||
|
||||
if (reader.Read<std::array<char, 4>>() != Magic) {
|
||||
throw std::runtime_error("Not a cooked model file");
|
||||
}
|
||||
|
||||
const auto version = reader.Read<std::uint32_t>();
|
||||
if (version != Version) {
|
||||
throw std::runtime_error(
|
||||
"Cooked model file has version " + std::to_string(version) +
|
||||
", expected " + std::to_string(Version) + " (recook the asset)");
|
||||
}
|
||||
reader.ReadHeader(Magic, Version, Kind);
|
||||
|
||||
ModelDoc::ModelAsset model;
|
||||
model.sourcePath = reader.ReadString();
|
||||
@@ -409,36 +274,49 @@ ModelDoc::ModelAsset ModelFile::Read(std::span<const std::uint8_t> bytes) {
|
||||
animation = ReadAnimation(reader);
|
||||
}
|
||||
|
||||
if (!reader.AtEnd()) {
|
||||
throw std::runtime_error("Cooked model file has unexpected data at the end");
|
||||
}
|
||||
reader.ExpectEnd(Kind);
|
||||
return model;
|
||||
}
|
||||
|
||||
void ModelFile::WriteToFile(const ModelDoc::ModelAsset& model, const std::filesystem::path& path) {
|
||||
const std::vector<std::uint8_t> bytes = Write(model);
|
||||
|
||||
std::ofstream file(path, std::ios::binary | std::ios::trunc);
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to open model file for writing: " + path.string());
|
||||
}
|
||||
file.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to write model file: " + path.string());
|
||||
}
|
||||
AssetBinary::WriteAllBytes(path, Write(model));
|
||||
}
|
||||
|
||||
ModelDoc::ModelAsset ModelFile::ReadFromFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to open model file: " + path.string());
|
||||
return Read(AssetBinary::ReadAllBytes(path));
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> ModelFile::ReadVersionFromFile(const std::filesystem::path& path) {
|
||||
return AssetBinary::ReadVersion(path, Magic);
|
||||
}
|
||||
|
||||
bool ModelFile::SupportsLoadOptions(const ModelDoc::ModelAsset& model, const ModelDoc::LoadOptions& options) {
|
||||
const bool sameImport =
|
||||
options.meshImportMode == CookOptions.meshImportMode &&
|
||||
options.flipUVs == CookOptions.flipUVs &&
|
||||
options.generateTangents == CookOptions.generateTangents &&
|
||||
options.generateSmoothNormals == CookOptions.generateSmoothNormals &&
|
||||
options.joinIdenticalVertices == CookOptions.joinIdenticalVertices &&
|
||||
options.limitBoneWeights == CookOptions.limitBoneWeights &&
|
||||
options.bakeStaticNodeTransforms == CookOptions.bakeStaticNodeTransforms;
|
||||
|
||||
// Meshes are built against the skeleton, so skinned models without it import differently
|
||||
const bool sameMeshes = options.loadSkeleton || !model.IsSkinned();
|
||||
|
||||
return sameImport && sameMeshes;
|
||||
}
|
||||
|
||||
void ModelFile::ApplyLoadOptions(ModelDoc::ModelAsset& model, const ModelDoc::LoadOptions& options) {
|
||||
if (!options.loadMaterials) {
|
||||
model.materials.clear();
|
||||
for (ModelDoc::MeshPrimitive& primitive : model.primitives) {
|
||||
primitive.materialIndex = ModelDoc::InvalidIndex;
|
||||
}
|
||||
}
|
||||
if (!options.loadSkeleton) {
|
||||
model.skeleton = Skeleton{};
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(file.tellg()));
|
||||
file.seekg(0);
|
||||
file.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to read model file: " + path.string());
|
||||
if (!options.loadAnimations) {
|
||||
model.animations.clear();
|
||||
}
|
||||
return Read(bytes);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#include <destrum/Assets/TextureFile.h>
|
||||
|
||||
#include <glm/gtc/packing.hpp>
|
||||
#include <glm/vec4.hpp>
|
||||
|
||||
#include "BinaryStream.h"
|
||||
|
||||
namespace {
|
||||
using AssetBinary::BinaryReader;
|
||||
using AssetBinary::BinaryWriter;
|
||||
|
||||
constexpr AssetBinary::Magic Magic{'D', 'T', 'E', 'X'};
|
||||
constexpr std::string_view Kind = "texture";
|
||||
|
||||
std::size_t ExpectedByteSize(const TextureFile::Texture& texture) {
|
||||
return std::size_t{texture.width} * texture.height * TextureFile::BytesPerPixel(texture.format);
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t TextureFile::BytesPerPixel(PixelFormat format) {
|
||||
switch (format) {
|
||||
case PixelFormat::RGBA16Float:
|
||||
return 4 * sizeof(std::uint16_t);
|
||||
}
|
||||
throw std::runtime_error("Unknown cooked texture pixel format");
|
||||
}
|
||||
|
||||
TextureFile::Texture TextureFile::FromRGBA32Float(std::uint32_t width, std::uint32_t height, std::span<const float> rgba) {
|
||||
const std::size_t pixelCount = std::size_t{width} * height;
|
||||
if (rgba.size() != pixelCount * 4) {
|
||||
throw std::invalid_argument("RGBA pixel data doesn't match the texture size");
|
||||
}
|
||||
|
||||
Texture texture{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.format = PixelFormat::RGBA16Float,
|
||||
};
|
||||
texture.pixels.resize(pixelCount * BytesPerPixel(texture.format));
|
||||
|
||||
for (std::size_t i = 0; i < pixelCount; ++i) {
|
||||
const glm::vec4 color{rgba[i * 4 + 0], rgba[i * 4 + 1], rgba[i * 4 + 2], rgba[i * 4 + 3]};
|
||||
const std::uint64_t packed = glm::packHalf4x16(color);
|
||||
std::memcpy(&texture.pixels[i * sizeof(packed)], &packed, sizeof(packed));
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> TextureFile::Write(const Texture& texture) {
|
||||
if (texture.pixels.size() != ExpectedByteSize(texture)) {
|
||||
throw std::invalid_argument("Texture pixel data doesn't match its size and format");
|
||||
}
|
||||
|
||||
BinaryWriter writer;
|
||||
writer.WriteHeader(Magic, Version);
|
||||
writer.Write(texture.width);
|
||||
writer.Write(texture.height);
|
||||
writer.Write(static_cast<std::uint32_t>(texture.format));
|
||||
writer.WriteArray(texture.pixels);
|
||||
return writer.TakeBytes();
|
||||
}
|
||||
|
||||
TextureFile::Texture TextureFile::Read(std::span<const std::uint8_t> bytes) {
|
||||
BinaryReader reader{bytes};
|
||||
reader.ReadHeader(Magic, Version, Kind);
|
||||
|
||||
Texture texture;
|
||||
texture.width = reader.Read<std::uint32_t>();
|
||||
texture.height = reader.Read<std::uint32_t>();
|
||||
texture.format = static_cast<PixelFormat>(reader.Read<std::uint32_t>());
|
||||
texture.pixels = reader.ReadArray<std::uint8_t>();
|
||||
reader.ExpectEnd(Kind);
|
||||
|
||||
if (texture.pixels.size() != ExpectedByteSize(texture)) {
|
||||
throw std::runtime_error("Cooked texture file is corrupt: pixel data doesn't match its size");
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
void TextureFile::WriteToFile(const Texture& texture, const std::filesystem::path& path) {
|
||||
AssetBinary::WriteAllBytes(path, Write(texture));
|
||||
}
|
||||
|
||||
TextureFile::Texture TextureFile::ReadFromFile(const std::filesystem::path& path) {
|
||||
return Read(AssetBinary::ReadAllBytes(path));
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> TextureFile::ReadVersionFromFile(const std::filesystem::path& path) {
|
||||
return AssetBinary::ReadVersion(path, Magic);
|
||||
}
|
||||
Reference in New Issue
Block a user