feat: add debug rendering for other types of colliders / split game into more logical functions / files

This commit is contained in:
2026-10-16 15:08:10 +02:00
parent 4c9b77c948
commit 469bb287b7
32 changed files with 42809 additions and 882 deletions
+289
View File
@@ -0,0 +1,289 @@
#include <scenes/MainScene.h>
#include <destrum/Assets/AssetManager.h>
#include <destrum/Components/Animator.h>
#include <destrum/Components/LightComponent.h>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Components/Physics/BoxCollider.h>
#include <destrum/Components/Physics/CapsuleCollider.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/ModelSpawner.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Util/ModelDocUtils.h>
#include <components/FirstPersonController.h>
#include <components/TriggerLoggerComponent.h>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/quaternion.hpp>
#include <spdlog/spdlog.h>
#include <memory>
#include <string>
namespace {
// The visible ground plane is at y = -1
constexpr float GroundHeight = -1.0f;
ModelDoc::LoadOptions StaticModelOptions()
{
ModelDoc::LoadOptions options{};
options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
options.loadMaterials = true;
options.loadSkeleton = false;
options.loadAnimations = false;
return options;
}
void CreateSun(Scene& scene)
{
// Directional lights shine down their local -Z: tilt it 50 degrees down, then turn it around the up axis
GameObject* sunObj = scene.CreateGameObject("Sun");
sunObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 10.f, 0.f));
sunObj->GetTransform().SetWorldRotation(
glm::angleAxis(glm::radians(35.f), glm::vec3{0.f, 1.f, 0.f}) *
glm::angleAxis(glm::radians(-50.f), glm::vec3{1.f, 0.f, 0.f}));
auto* sun = sunObj->AddComponent<LightComponent>(LightType::Directional);
sun->SetColor(glm::vec3{1.0f, 0.95f, 0.85f});
sun->SetIntensity(3.0f);
sun->SetCastShadows(true);
}
void CreateGround(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
const auto planeModel = AssetManager::GetInstance().LoadModel("game://plane.glb", StaticModelOptions());
ModelDocUtils::LogModelDocSummary(planeModel, "plane.glb");
const auto& planePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(planeModel, "game://plane.glb");
const auto planeTexturePath = ModelDocUtils::PickTexturePath(
planeModel,
planePrimitive,
AssetFS::GetInstance().GetCookedPathForFile("engine://grass.jpg"));
const MaterialID planeMaterialID = resources.materials().addMaterial({
.baseColor = ModelDocUtils::GetImportedBaseColor(planeModel, planePrimitive),
.textureFilteringMode = TextureFilteringMode::Nearest,
.diffuseTexture = resources.loadImageFromFile(gfxDevice, planeTexturePath),
.name = ModelDocUtils::GetImportedMaterialName(planeModel, planePrimitive, "GroundPlaneMaterial"),
});
GameObject* planeObj = scene.CreateGameObject("GroundPlane");
auto* planeMeshComp = planeObj->AddComponent<MeshRendererComponent>();
planeMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, planePrimitive.mesh));
planeMeshComp->SetMaterialID(planeMaterialID);
planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, GroundHeight, 0.f));
planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f));
// The plane mesh is flat, so the floor collider sits right below it
GameObject* groundObj = scene.CreateGameObject("GroundCollider");
groundObj->GetTransform().SetWorldPosition(glm::vec3(0.f, GroundHeight - 0.5f, 0.f));
groundObj->AddComponent<BoxCollider>(glm::vec3{10.0f, 0.5f, 10.0f});
groundObj->AddComponent<Rigidbody>()->SetType(RigidbodyType::Static);
scene.GetPhysics().RegisterGameObject(*groundObj);
}
// Kenney prototype textures, texture_01 is a 1 x 1 meter grid
MaterialID AddPrototypeMaterial(GfxDevice& gfxDevice, RenderResources& resources,
const std::string& color, float roughness)
{
Material material = Material::SimpleColor(glm::vec3{1.0f}, "Prototype" + color);
material.roughnessFactor = roughness;
material.textureFilteringMode = TextureFilteringMode::Anisotropic;
material.diffuseTexture = resources.loadImageFromFile(
gfxDevice, AssetFS::GetInstance().GetFullPath("engine://kenny/" + color + "/texture_01.png"));
return resources.materials().addMaterial(std::move(material));
}
GameObject* CreateBox(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources,
const std::string& name, const glm::vec3& position, const glm::vec3& size,
MaterialID materialID, RigidbodyType type)
{
GameObject* box = scene.CreateGameObject(name);
box->GetTransform().SetWorldPosition(position);
auto* boxMeshComp = box->AddComponent<MeshRendererComponent>();
boxMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, PrimitiveMeshes::MakeBox(size)));
boxMeshComp->SetMaterialID(materialID);
box->AddComponent<BoxCollider>(size * 0.5f);
auto* boxRb = box->AddComponent<Rigidbody>();
boxRb->SetType(type);
boxRb->SetMass(10.0f);
scene.GetPhysics().RegisterGameObject(*box);
return box;
}
// Every platform is reachable with a jump from the previous one
void CreateParkourCourse(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
const MaterialID platformMaterial = AddPrototypeMaterial(gfxDevice, resources, "Orange", 0.6f);
const MaterialID goalMaterial = AddPrototypeMaterial(gfxDevice, resources, "Green", 0.6f);
const MaterialID wallMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f);
const MaterialID crateMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.35f);
// Platforms stand on the ground and are given by their top height
const auto createPlatform = [&](const std::string& name, const glm::vec2& center, float top,
const glm::vec2& footprint, MaterialID materialID)
{
const float height = top - GroundHeight;
CreateBox(scene, gfxDevice, resources, name,
glm::vec3{center.x, top - height * 0.5f, center.y},
glm::vec3{footprint.x, height, footprint.y},
materialID, RigidbodyType::Static);
};
createPlatform("Parkour_Step", {0.0f, 4.0f}, -0.5f, {2.0f, 2.0f}, platformMaterial);
createPlatform("Parkour_Block", {0.0f, 7.0f}, 0.4f, {2.0f, 2.0f}, platformMaterial);
createPlatform("Parkour_Pillar1", {3.5f, 8.5f}, 1.2f, {1.5f, 1.5f}, platformMaterial);
createPlatform("Parkour_Pillar2", {6.5f, 7.5f}, 2.0f, {1.2f, 1.2f}, platformMaterial);
CreateBox(scene, gfxDevice, resources, "Parkour_Beam", {6.5f, 1.85f, 4.0f}, {0.6f, 0.3f, 5.0f},
platformMaterial, RigidbodyType::Static);
createPlatform("Parkour_Goal", {6.5f, -0.5f}, 2.8f, {2.5f, 2.5f}, goalMaterial);
CreateBox(scene, gfxDevice, resources, "Wall", {-5.0f, 0.5f, 0.0f}, {0.5f, 3.0f, 8.0f},
wallMaterial, RigidbodyType::Static);
CreateBox(scene, gfxDevice, resources, "LowWall", {-2.5f, -0.6f, -4.0f}, {4.0f, 0.8f, 0.4f},
wallMaterial, RigidbodyType::Static);
for (int i = 0; i < 4; ++i)
{
CreateBox(scene, gfxDevice, resources, "Crate" + std::to_string(i),
{-2.5f, -0.6f + 0.81f * static_cast<float>(i), 2.0f},
glm::vec3{0.8f}, crateMaterial, RigidbodyType::Dynamic);
}
}
void CreateFlightHelmet(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
GameObject* helmetObj = ModelSpawner::SpawnStaticModel(
scene, gfxDevice, resources,
"engine://flight-helmet/FlightHelmet.gltf",
StaticModelOptions(),
"FlightHelmet");
helmetObj->GetTransform().SetWorldPosition(glm::vec3(2.f, GroundHeight, 0.f));
helmetObj->GetTransform().SetWorldScale(glm::vec3(2.f));
}
void CreateTree(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
constexpr float scale = 3.0f;
GameObject* treeObj = ModelSpawner::SpawnStaticModel(
scene, gfxDevice, resources,
"engine://tree/base.obj",
StaticModelOptions(),
"Tree");
treeObj->GetTransform().SetWorldPosition(glm::vec3(-7.f, GroundHeight + scale, 7.f));
treeObj->GetTransform().SetWorldScale(glm::vec3(scale));
// The trunk is about 0.14 units thick and runs up through the foliage, colliders are in world units
treeObj->AddComponent<CapsuleCollider>(0.15f * scale, 2.0f * scale);
treeObj->AddComponent<Rigidbody>()->SetType(RigidbodyType::Static);
scene.GetPhysics().RegisterGameObject(*treeObj);
}
void CreateCapybara(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
constexpr auto modelReference = "engine://cotw-capybara-male/source/capybara.fbx";
ModelDoc::LoadOptions options{};
options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
options.loadMaterials = true;
options.loadSkeleton = true;
options.loadAnimations = true;
auto charModel = AssetManager::GetInstance().LoadModel(modelReference, options);
ModelDocUtils::LogModelDocSummary(charModel, "capybara.fbx");
const auto& charPrimitive = ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow(charModel, modelReference);
// The model stores an absolute texture path from another machine, so fall back to the shipped texture
const auto charTexturePath = ModelDocUtils::PickTexturePath(
charModel,
charPrimitive,
AssetFS::GetInstance().GetFullPath(
"engine://cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.pngballs"));
const MaterialID charMaterialID = resources.materials().addMaterial({
.baseColor = ModelDocUtils::GetImportedBaseColor(charModel, charPrimitive),
.diffuseTexture = resources.loadImageFromFile(gfxDevice, charTexturePath),
.name = ModelDocUtils::GetImportedMaterialName(charModel, charPrimitive, "CharacterMaterial"),
});
GameObject* charObj = scene.CreateGameObject("Character");
auto* charMeshComp = charObj->AddComponent<MeshRendererComponent>();
charMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, charPrimitive.mesh));
charMeshComp->SetMaterialID(charMaterialID);
auto* animator = charObj->AddComponent<Animator>();
animator->setSkeleton(std::move(charModel.skeleton));
for (auto& clip : charModel.animations)
{
spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration);
animator->addClip(std::make_shared<SkeletalAnimation>(std::move(clip)));
}
animator->play("capybara_canter_fwd_01|capybara_canter_fwd_01|run");
charObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f));
charObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f);
}
// Logs everything that enters or leaves it, spheres from the "Spawn ball" button fall through it
void CreateTriggerZone(Scene& scene)
{
GameObject* triggerObj = scene.CreateGameObject("TriggerBox");
triggerObj->AddComponent<BoxCollider>(glm::vec3{3.0f, 3.0f, 3.0f})->SetTrigger(true);
triggerObj->AddComponent<Rigidbody>()->SetType(RigidbodyType::Static);
triggerObj->AddComponent<TriggerLoggerComponent>();
triggerObj->GetTransform().SetWorldPosition({0.0f, 0.0f, 0.0f});
}
void CreatePlayer(Scene& scene, MainSceneObjects& objects)
{
GameObject* playerObj = scene.CreateGameObject("Player");
playerObj->GetTransform().SetWorldPosition(glm::vec3(0.0f, 0.0f, -6.0f));
playerObj->AddComponent<CapsuleCollider>(0.4f, 1.8f);
playerObj->AddComponent<Rigidbody>();
// Facing the parkour course
auto* controller = playerObj->AddComponent<FirstPersonController>();
controller->SetYaw(glm::half_pi<float>());
scene.GetPhysics().RegisterGameObject(*playerObj);
objects.player = playerObj;
objects.playerController = controller;
}
void LoadSpawnBallResources(GfxDevice& gfxDevice, RenderResources& resources, MainSceneObjects& objects)
{
const auto sphereModel = AssetManager::GetInstance().LoadModel("engine://sphere.fbx", StaticModelOptions());
ModelDocUtils::LogModelDocSummary(sphereModel, "sphere.fbx");
const auto& spherePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(sphereModel, "engine://sphere.fbx");
objects.sphereMesh = resources.meshes().addMesh(gfxDevice, spherePrimitive.mesh);
objects.sphereMaterial = resources.materials().addSimpleColorMaterial({0.5f, 0.3f, 0.8f}, "Blue");
}
}
MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
MainSceneObjects objects{};
scene.GetEnvironment().fogDensity = 0.01f;
CreateSun(scene);
CreateGround(scene, gfxDevice, resources);
CreateParkourCourse(scene, gfxDevice, resources);
CreateFlightHelmet(scene, gfxDevice, resources);
CreateTree(scene, gfxDevice, resources);
CreateCapybara(scene, gfxDevice, resources);
CreateTriggerZone(scene);
CreatePlayer(scene, objects);
LoadSpawnBallResources(gfxDevice, resources, objects);
return objects;
}