feat: Add Camera component / Add cooked model and texture loading
This commit is contained in:
+187
-3
@@ -1,14 +1,19 @@
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <glm/gtc/packing.hpp>
|
||||
|
||||
#include <destrum/Event.h>
|
||||
#include <destrum/Assets/AssetManager.h>
|
||||
#include <destrum/Assets/AssetReference.h>
|
||||
#include <destrum/Assets/ModelFile.h>
|
||||
#include <destrum/Assets/TextureFile.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
@@ -23,9 +28,11 @@
|
||||
#include <destrum/Components/Physics/BoxCollider.h>
|
||||
#include <destrum/Components/Physics/Collider.h>
|
||||
#include <destrum/Components/Animator.h>
|
||||
#include <destrum/Components/CameraComponent.h>
|
||||
#include <destrum/Components/LightComponent.h>
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
#include <destrum/Graphics/Material.h>
|
||||
#include <destrum/Graphics/imageLoader.h>
|
||||
#include <destrum/Graphics/Managers/LineRenderingManager.h>
|
||||
|
||||
namespace {
|
||||
@@ -486,6 +493,48 @@ namespace {
|
||||
"LightComponent range must stay positive");
|
||||
}
|
||||
|
||||
void testSceneCurrentCamera()
|
||||
{
|
||||
Scene& scene = SceneManager::GetInstance().CreateScene("cameras");
|
||||
check(scene.GetCurrentCamera() == nullptr, "a scene without cameras must have no current camera");
|
||||
|
||||
GameObject* first = scene.CreateGameObject("first camera");
|
||||
auto* firstCamera = first->AddComponent<CameraComponent>();
|
||||
scene.CommitPendingAdditions();
|
||||
check(scene.GetCurrentCamera() == firstCamera, "the only camera in a scene must be the current camera");
|
||||
|
||||
GameObject* second = scene.CreateGameObject("second camera");
|
||||
auto* secondCamera = second->AddComponent<CameraComponent>();
|
||||
secondCamera->SetFieldOfView(60.0f);
|
||||
second->GetTransform().SetWorldPosition({1.0f, 2.0f, 3.0f});
|
||||
scene.CommitPendingAdditions();
|
||||
secondCamera->MakeCurrent();
|
||||
check(scene.GetCurrentCamera() == secondCamera && !firstCamera->IsCurrent(),
|
||||
"the selected camera must be the current camera");
|
||||
|
||||
const Camera& camera = secondCamera->GetCamera(16.0f / 9.0f);
|
||||
check(camera.GetPosition() == glm::vec3{1.0f, 2.0f, 3.0f} &&
|
||||
close(camera.GetForward().z, -1.0f) && camera.GetFOVY() == 60.0f,
|
||||
"a camera component must place its camera at its transform, looking down -Z");
|
||||
|
||||
const auto path = std::filesystem::temp_directory_path() / "destrum_camera_scene_test.json";
|
||||
check(SceneSerializer::Save(scene, path), "scene with cameras must save");
|
||||
check(SceneSerializer::Load(scene, path), "scene with cameras must load");
|
||||
std::filesystem::remove(path);
|
||||
|
||||
const CameraComponent* loadedCamera = scene.GetCurrentCamera();
|
||||
check(loadedCamera != nullptr && loadedCamera->GetGameObject()->GetName() == "second camera" &&
|
||||
loadedCamera->GetFieldOfView() == 60.0f,
|
||||
"scene round-trip must preserve the current camera");
|
||||
|
||||
scene.GetCurrentCamera()->GetGameObject()->SetActive(false);
|
||||
check(scene.GetCurrentCamera() != nullptr &&
|
||||
scene.GetCurrentCamera()->GetGameObject()->GetName() == "first camera",
|
||||
"an inactive current camera must fall back to another camera");
|
||||
|
||||
SceneManager::GetInstance().Destroy();
|
||||
}
|
||||
|
||||
ModelDoc::ModelAsset makeTestModelAsset()
|
||||
{
|
||||
Skeleton skeleton;
|
||||
@@ -535,11 +584,11 @@ namespace {
|
||||
material.opacity = 0.5f;
|
||||
material.textures = {{
|
||||
.semantic = ModelDoc::TextureSemantic::BaseColor,
|
||||
.rawPath = "*0",
|
||||
.rawPath = "skin.png",
|
||||
.path = "textures/skin.png",
|
||||
.assimpType = aiTextureType_DIFFUSE,
|
||||
.slot = 1,
|
||||
.embedded = true,
|
||||
.embedded = false,
|
||||
}};
|
||||
|
||||
SkeletalAnimation animation;
|
||||
@@ -610,7 +659,8 @@ namespace {
|
||||
check(texture.semantic == ModelDoc::TextureSemantic::BaseColor &&
|
||||
texture.path == "textures/skin.png" &&
|
||||
texture.assimpType == aiTextureType_DIFFUSE &&
|
||||
texture.embedded,
|
||||
texture.slot == 1 &&
|
||||
!texture.embedded,
|
||||
"cooked materials must keep their textures");
|
||||
|
||||
check(loaded.skeleton.jointNames == std::vector<std::string>{"root", "spine"} &&
|
||||
@@ -669,6 +719,136 @@ namespace {
|
||||
"a cooked imported model must match the import exactly");
|
||||
}
|
||||
|
||||
bool loadModelThrows(std::string_view reference, const ModelDoc::LoadOptions& options)
|
||||
{
|
||||
try {
|
||||
(void)AssetManager::GetInstance().LoadModel(reference, options);
|
||||
} catch (const std::exception&) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void testAssetManagerLoadsCookedModels()
|
||||
{
|
||||
const auto root = std::filesystem::temp_directory_path() / "destrum_cooked_model_test";
|
||||
const auto engineDir = root / "assets" / "engine";
|
||||
std::filesystem::remove_all(root);
|
||||
std::filesystem::create_directories(engineDir);
|
||||
std::filesystem::create_directories(root / "assets" / "game");
|
||||
|
||||
// The source can't be imported, so a successful load has to come from the cooked file
|
||||
std::ofstream{engineDir / "model.fbx"} << "not a model";
|
||||
ModelFile::WriteToFile(makeTestModelAsset(), engineDir / "model.fbx.dmodel");
|
||||
std::ofstream{engineDir / "manifest.json"}
|
||||
<< R"({"version":1,"assets":[{"src":"model.fbx","out":"model.fbx.dmodel","type":"model","mtime_epoch_ns":0,"size_bytes":0}]})";
|
||||
|
||||
auto& assetFs = AssetFS::GetInstance();
|
||||
assetFs.Init(root);
|
||||
|
||||
const auto model = AssetManager::GetInstance().LoadModel("engine://model.fbx");
|
||||
check(model.primitives.size() == 1 && model.animations.size() == 1,
|
||||
"cooked models must be loaded instead of importing the source");
|
||||
check(model.sourcePath == "engine://model.fbx" &&
|
||||
model.primitives.front().mesh.assetReference.path == "engine://model.fbx" &&
|
||||
model.primitives.front().mesh.skeleton->assetReference.path == "engine://model.fbx",
|
||||
"cooked models must be referenced by their virtual path");
|
||||
check(model.materials.front().textures.front().path ==
|
||||
std::filesystem::weakly_canonical(engineDir / "textures/skin.png").generic_string(),
|
||||
"cooked texture paths must be resolved next to the cooked model");
|
||||
|
||||
ModelDoc::LoadOptions withoutExtras{};
|
||||
withoutExtras.loadMaterials = false;
|
||||
withoutExtras.loadAnimations = false;
|
||||
const auto stripped = AssetManager::GetInstance().LoadModel("engine://model.fbx", withoutExtras);
|
||||
check(stripped.materials.empty() &&
|
||||
stripped.animations.empty() &&
|
||||
stripped.primitives.front().materialIndex == ModelDoc::InvalidIndex,
|
||||
"cooked models must drop what the load options didn't ask for");
|
||||
|
||||
ModelDoc::LoadOptions withoutSkeleton{};
|
||||
withoutSkeleton.loadSkeleton = false;
|
||||
check(loadModelThrows("engine://model.fbx", withoutSkeleton),
|
||||
"skinned cooked models must be imported when loaded without a skeleton");
|
||||
|
||||
ModelDoc::LoadOptions otherImport{};
|
||||
otherImport.flipUVs = false;
|
||||
check(loadModelThrows("engine://model.fbx", otherImport),
|
||||
"cooked models must be imported when the import options differ from the cook options");
|
||||
|
||||
assetFs.Reset();
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
TextureFile::Texture makeTestTexture()
|
||||
{
|
||||
const std::vector<float> rgba{
|
||||
0.0f, 0.25f, 0.5f, 1.0f,
|
||||
2.0f, 16.0f, 100.0f, 1.0f,
|
||||
};
|
||||
return TextureFile::FromRGBA32Float(2, 1, rgba);
|
||||
}
|
||||
|
||||
bool textureFileReadThrows(const std::vector<std::uint8_t>& bytes)
|
||||
{
|
||||
try {
|
||||
(void)TextureFile::Read(bytes);
|
||||
} catch (const std::exception&) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void testTextureFileRoundTrip()
|
||||
{
|
||||
const auto texture = makeTestTexture();
|
||||
check(texture.pixels.size() == 2 * TextureFile::BytesPerPixel(TextureFile::PixelFormat::RGBA16Float),
|
||||
"cooked textures must store every pixel");
|
||||
|
||||
std::uint64_t secondPixel = 0;
|
||||
std::memcpy(&secondPixel, texture.pixels.data() + 8, sizeof(secondPixel));
|
||||
const glm::vec4 unpacked = glm::unpackHalf4x16(secondPixel);
|
||||
check(unpacked == glm::vec4{2.0f, 16.0f, 100.0f, 1.0f},
|
||||
"half float conversion must keep HDR values above 1");
|
||||
|
||||
const auto bytes = TextureFile::Write(texture);
|
||||
const auto loaded = TextureFile::Read(bytes);
|
||||
check(loaded.width == 2 && loaded.height == 1 &&
|
||||
loaded.format == TextureFile::PixelFormat::RGBA16Float &&
|
||||
loaded.pixels == texture.pixels,
|
||||
"cooked textures must round-trip");
|
||||
|
||||
auto truncated = bytes;
|
||||
truncated.resize(bytes.size() - 1);
|
||||
check(textureFileReadThrows(truncated), "truncated cooked textures must be rejected");
|
||||
|
||||
auto wrongMagic = bytes;
|
||||
wrongMagic[0] = 'X';
|
||||
check(textureFileReadThrows(wrongMagic), "files that aren't cooked textures must be rejected");
|
||||
|
||||
auto wrongSize = bytes;
|
||||
wrongSize[8] = 3;
|
||||
check(textureFileReadThrows(wrongSize), "cooked textures whose size doesn't match their pixels must be rejected");
|
||||
}
|
||||
|
||||
void testImageLoaderReadsCookedTextures()
|
||||
{
|
||||
const auto path = std::filesystem::temp_directory_path() / "destrum_texture_test.dtex";
|
||||
const auto texture = makeTestTexture();
|
||||
TextureFile::WriteToFile(texture, path);
|
||||
|
||||
const auto image = util::loadImage(path, TextureIntent::ColorSrgb);
|
||||
std::filesystem::remove(path);
|
||||
|
||||
check(image.vkFormat == VK_FORMAT_R16G16B16A16_SFLOAT &&
|
||||
image.width == 2 && image.height == 1 &&
|
||||
image.byteSize == texture.pixels.size(),
|
||||
"cooked textures must load as half float images");
|
||||
check(image.pixels != nullptr &&
|
||||
std::memcmp(image.pixels, texture.pixels.data(), texture.pixels.size()) == 0,
|
||||
"cooked texture pixels must be uploaded as stored");
|
||||
}
|
||||
|
||||
void testSimpleColorMaterial()
|
||||
{
|
||||
const auto material = Material::SimpleColor(
|
||||
@@ -787,9 +967,13 @@ int main()
|
||||
testModelFileRoundTrip();
|
||||
testModelFileRejectsBadData();
|
||||
testModelFileFromImportedModel();
|
||||
testAssetManagerLoadsCookedModels();
|
||||
testTextureFileRoundTrip();
|
||||
testImageLoaderReadsCookedTextures();
|
||||
testSimpleColorMaterial();
|
||||
testAssetReferenceCacheKeys();
|
||||
testTriggerZone();
|
||||
testSceneCurrentCamera();
|
||||
std::cout << "destrum tests passed\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user