feat: move editor to project / add post processing etc
This commit is contained in:
+97
-165
@@ -1,53 +1,44 @@
|
||||
#include "Lightkeeper.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <destrum/Components/CameraComponent.h>
|
||||
#include <destrum/Components/MeshRendererComponent.h>
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
#include <destrum/Components/Physics/SphereCollider.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
#include <destrum/Serialization/SceneSerializer.h>
|
||||
|
||||
#include <components/FirstPersonController.h>
|
||||
#include <components/GameComponentRegistry.h>
|
||||
|
||||
LightKeeper::LightKeeper() : App()
|
||||
{
|
||||
}
|
||||
|
||||
LightKeeper::~LightKeeper()
|
||||
{
|
||||
if (!cleanedUp) {
|
||||
try {
|
||||
cleanup();
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::customInit()
|
||||
{
|
||||
RegisterGameComponents();
|
||||
|
||||
resources.init(gfxDevice);
|
||||
renderer.init(gfxDevice, resources, m_params.renderSize);
|
||||
|
||||
const float aspectRatio = static_cast<float>(m_params.renderSize.x) / static_cast<float>(m_params.renderSize.y);
|
||||
camera.setAspectRatio(aspectRatio);
|
||||
camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f)));
|
||||
window.Maximize();
|
||||
|
||||
setupSkybox();
|
||||
|
||||
auto& scene = SceneManager::GetInstance().CreateScene("Main");
|
||||
mainScene = BuildMainScene(scene, gfxDevice, resources);
|
||||
|
||||
debug2DTexture = resources.loadImageFromFile(
|
||||
gfxDevice, AssetFS::GetInstance().GetFullPath("engine://textures/kobe.png"));
|
||||
|
||||
Scene& scene = SceneManager::GetInstance().CreateScene("Main");
|
||||
if (!SceneSerializer::Load(scene, AssetFS::GetInstance().GetFullPath(MainScenePath))) {
|
||||
throw std::runtime_error("Could not load " + std::string{MainScenePath});
|
||||
}
|
||||
onPlayModeChanged(isPlaying());
|
||||
}
|
||||
|
||||
void LightKeeper::setupSkybox()
|
||||
{
|
||||
const auto skyboxPath = AssetFS::GetInstance().GetCookedPathForFile("engine://DJI_0001_stitch.jpg");
|
||||
const auto vertShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.vert");
|
||||
const auto fragShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.frag");
|
||||
auto& assetFs = AssetFS::GetInstance();
|
||||
const auto skyboxPath = assetFs.GetCookedPathForFile("engine://DJI_0001_stitch.jpg");
|
||||
const auto vertShaderPath = assetFs.GetCookedPathForFile("engine://shaders/cubemap.vert");
|
||||
const auto fragShaderPath = assetFs.GetCookedPathForFile("engine://shaders/cubemap.frag");
|
||||
|
||||
skyboxCubemap = std::make_unique<CubeMap>();
|
||||
skyboxCubemap->LoadCubeMap(gfxDevice, resources, skyboxPath.generic_string());
|
||||
@@ -57,161 +48,102 @@ void LightKeeper::setupSkybox()
|
||||
renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID());
|
||||
}
|
||||
|
||||
void LightKeeper::customUpdate(float dt)
|
||||
void LightKeeper::customUpdate(float)
|
||||
{
|
||||
updateViewMode();
|
||||
|
||||
// LightKeeper owns a camera that hides App::camera; update this instance
|
||||
// after SDL events have been consumed.
|
||||
if (!isPlayerViewActive() && getSceneCamera() == nullptr)
|
||||
{
|
||||
if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F))
|
||||
{
|
||||
focusSelection();
|
||||
}
|
||||
updateFocus(dt);
|
||||
camera.Update(dt);
|
||||
}
|
||||
if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F11))
|
||||
{
|
||||
const auto& input = InputManager::GetInstance();
|
||||
if (input.WasKeyPressed(SDL_SCANCODE_F11)) {
|
||||
window.ToggleFullscreen();
|
||||
}
|
||||
|
||||
SceneManager::GetInstance().Update(dt);
|
||||
SceneManager::GetInstance().LateUpdate(dt);
|
||||
|
||||
updateDebugUi();
|
||||
}
|
||||
|
||||
void LightKeeper::updateViewMode()
|
||||
{
|
||||
auto& input = InputManager::GetInstance();
|
||||
FirstPersonController* player = mainScene.playerController;
|
||||
|
||||
if (isPlaying() && !isPaused() && player != nullptr && input.WasKeyPressed(SDL_SCANCODE_F6))
|
||||
{
|
||||
player->SetPossessed(!player->IsPossessed());
|
||||
if (playerController != nullptr && isPlaying() && !isPaused() && input.WasKeyPressed(SDL_SCANCODE_F6)) {
|
||||
playerController->SetPossessed(!playerController->IsPossessed());
|
||||
}
|
||||
|
||||
if (!isPlayerViewActive())
|
||||
{
|
||||
const bool looking = input.IsMouseDown(SDL_BUTTON_RIGHT);
|
||||
if (looking != input.IsMouseCaptured())
|
||||
{
|
||||
input.SetMouseCaptured(looking);
|
||||
}
|
||||
if (showTestWindow) {
|
||||
drawTestWindow();
|
||||
}
|
||||
}
|
||||
|
||||
bool LightKeeper::isPlayerViewActive() const
|
||||
{
|
||||
return isPlaying() && !isPaused() && mainScene.playerController != nullptr && mainScene.playerController->IsPossessed();
|
||||
}
|
||||
|
||||
CameraComponent* LightKeeper::getSceneCamera() const
|
||||
{
|
||||
const auto& activeScenes = SceneManager::GetInstance().GetActiveScenes();
|
||||
if (!isPlaying() || useFreeCamera || activeScenes.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
return activeScenes.front()->GetCurrentCamera();
|
||||
}
|
||||
|
||||
Camera& LightKeeper::getViewCamera()
|
||||
{
|
||||
CameraComponent* sceneCamera = isPlayerViewActive() ? nullptr : getSceneCamera();
|
||||
return sceneCamera != nullptr ? sceneCamera->GetCamera(camera.GetAspectRatio()) : camera;
|
||||
}
|
||||
|
||||
void LightKeeper::customDraw()
|
||||
{
|
||||
const auto cmd = gfxDevice.beginFrame();
|
||||
if (cmd == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.beginDrawing(gfxDevice);
|
||||
submitDebugDraws();
|
||||
|
||||
if (isPlayerViewActive())
|
||||
{
|
||||
camera.SetRotation(mainScene.playerController->GetYaw(), mainScene.playerController->GetPitch());
|
||||
camera.m_position = mainScene.playerController->GetEyePosition();
|
||||
camera.CalculateViewMatrix();
|
||||
camera.CalculateProjectionMatrix();
|
||||
}
|
||||
|
||||
Camera& viewCamera = getViewCamera();
|
||||
|
||||
const SceneEnvironment& environment = SceneManager::GetInstance().GetCurrentScene().GetEnvironment();
|
||||
const GameRenderer::SceneData sceneData{
|
||||
.camera = viewCamera,
|
||||
.ambientColor = environment.ambientColor,
|
||||
.ambientIntensity = environment.ambientIntensity,
|
||||
.fogColor = environment.fogColor,
|
||||
.fogDensity = environment.fogDensity,
|
||||
.skyboxRotation = environment.skyboxRotation,
|
||||
};
|
||||
|
||||
const RenderContext ctx{
|
||||
.renderer = renderer,
|
||||
.camera = viewCamera,
|
||||
.sceneData = sceneData,
|
||||
};
|
||||
|
||||
SceneManager::GetInstance().Render(ctx);
|
||||
renderer.endDrawing();
|
||||
|
||||
const auto& drawImage = renderer.getDrawImage();
|
||||
|
||||
renderer.draw(cmd, gfxDevice, viewCamera, sceneData);
|
||||
|
||||
gfxDevice.endFrame(
|
||||
cmd, drawImage, {
|
||||
.clearColor = {{0.f, 0.f, 0.5f, 1.f}},
|
||||
.drawImageBlitRect = glm::ivec4{},
|
||||
.imguiPass = &imguiPass,
|
||||
.copyDrawImageToSwapchain = !usesEditorViewport(),
|
||||
});
|
||||
}
|
||||
|
||||
void LightKeeper::customCleanup()
|
||||
{
|
||||
SceneManager::GetInstance().Destroy();
|
||||
|
||||
if (skyboxCubemap)
|
||||
{
|
||||
if (skyboxCubemap) {
|
||||
skyboxCubemap->cleanup(gfxDevice);
|
||||
skyboxCubemap.reset();
|
||||
}
|
||||
|
||||
renderer.cleanup(gfxDevice);
|
||||
}
|
||||
|
||||
void LightKeeper::customFixedUpdate(float dt)
|
||||
void LightKeeper::customWindowsMenu()
|
||||
{
|
||||
SceneManager::GetInstance().FixedUpdate(dt);
|
||||
ImGui::MenuItem("Test", nullptr, &showTestWindow);
|
||||
}
|
||||
|
||||
void LightKeeper::onPlayModeChanged(bool nowPlaying)
|
||||
void LightKeeper::onPlayModeChanged(bool playing)
|
||||
{
|
||||
findPlayer();
|
||||
if (nowPlaying) {
|
||||
// The player view moves the free camera, put it back where the editor left it on Stop
|
||||
editorCamera = camera;
|
||||
if (mainScene.playerController != nullptr) {
|
||||
mainScene.playerController->SetPossessed(true);
|
||||
}
|
||||
} else if (editorCamera) {
|
||||
const float aspectRatio = camera.GetAspectRatio();
|
||||
camera = *editorCamera;
|
||||
camera.setAspectRatio(aspectRatio);
|
||||
editorCamera.reset();
|
||||
if (playing && playerController != nullptr) {
|
||||
playerController->SetPossessed(true);
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::onViewportResize(int newWidth, int newHeight)
|
||||
Camera* LightKeeper::gameCamera()
|
||||
{
|
||||
const float aspectRatio = static_cast<float>(newWidth) / static_cast<float>(newHeight);
|
||||
camera.setAspectRatio(aspectRatio);
|
||||
if (playerController != nullptr && playerController->IsPossessed() && isPlaying() && !isPaused()) {
|
||||
playerCamera.SetRotation(playerController->GetYaw(), playerController->GetPitch());
|
||||
playerCamera.m_position = playerController->GetEyePosition();
|
||||
playerCamera.CalculateViewMatrix();
|
||||
return &playerCamera;
|
||||
}
|
||||
|
||||
auto& sceneManager = SceneManager::GetInstance();
|
||||
if (sceneManager.GetActiveScenes().empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
CameraComponent* sceneCamera = sceneManager.GetCurrentScene().GetCurrentCamera();
|
||||
return sceneCamera != nullptr ? &sceneCamera->GetCamera(getRenderAspectRatio()) : nullptr;
|
||||
}
|
||||
|
||||
// Loading or switching scenes replaces the player, so look it up again instead of keeping a dangling pointer
|
||||
void LightKeeper::findPlayer()
|
||||
{
|
||||
playerController = nullptr;
|
||||
auto& sceneManager = SceneManager::GetInstance();
|
||||
if (sceneManager.GetActiveScenes().empty()) {
|
||||
return;
|
||||
}
|
||||
for (const auto& object : sceneManager.GetCurrentScene().GetObjects()) {
|
||||
if (object != nullptr && !object->IsBeingDestroyed()) {
|
||||
if (auto* controller = object->GetComponent<FirstPersonController>()) {
|
||||
playerController = controller;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::drawTestWindow()
|
||||
{
|
||||
if (ImGui::Begin("Test", &showTestWindow) && ImGui::Button("Spawn ball")) {
|
||||
spawnBall();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void LightKeeper::spawnBall()
|
||||
{
|
||||
Scene& scene = SceneManager::GetInstance().GetCurrentScene();
|
||||
constexpr float radius = 0.75f;
|
||||
|
||||
GameObject* ball = scene.CreateGameObject("Ball");
|
||||
ball->GetTransform().SetWorldPosition({static_cast<float>(std::rand() % 5) - 2.0f, 20.0f, static_cast<float>(std::rand() % 5) - 2.0f});
|
||||
ball->AddComponent<SphereCollider>(radius);
|
||||
ball->AddComponent<Rigidbody>()->SetMass(50.0f);
|
||||
|
||||
const auto desc = PrimitiveMeshes::Desc{.shape = PrimitiveMeshes::Shape::Sphere, .radius = radius};
|
||||
auto mesh = resources.meshes().findMeshByKey(PrimitiveMeshes::MakeKey(desc));
|
||||
if (!mesh) {
|
||||
mesh = resources.meshes().addMesh(gfxDevice, PrimitiveMeshes::Make(desc));
|
||||
}
|
||||
auto* meshRenderer = ball->AddComponent<MeshRendererComponent>();
|
||||
meshRenderer->SetMeshID(*mesh);
|
||||
meshRenderer->SetMaterialID(resources.materials().addSimpleColorMaterial({0.5f, 0.3f, 0.8f}, "Ball"));
|
||||
|
||||
scene.GetPhysics().RefreshGameObject(*ball);
|
||||
}
|
||||
|
||||
@@ -1,552 +0,0 @@
|
||||
#include "Lightkeeper.h"
|
||||
|
||||
#include <destrum/Components/CameraComponent.h>
|
||||
#include <destrum/Components/MeshRendererComponent.h>
|
||||
#include <destrum/Components/Physics/Collider.h>
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
#include <destrum/Components/Physics/SphereCollider.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/Managers/LineRenderingManager.h>
|
||||
#include <destrum/Graphics/Managers/Renderer2D.h>
|
||||
#include <destrum/Input/InputManager.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Scene/ModelSpawner.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Util/AssetBrowser.h>
|
||||
#include <destrum/Util/ModelDoc.h>
|
||||
#include <destrum/Serialization/PrefabSerializer.h>
|
||||
#include <destrum/Serialization/SceneSerializer.h>
|
||||
|
||||
#include <components/FirstPersonController.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "spdlog/spdlog.h"
|
||||
|
||||
namespace {
|
||||
std::filesystem::path ResolveScenePath(
|
||||
std::string_view value,
|
||||
const std::filesystem::path& exeDir) {
|
||||
if (value.empty()) {
|
||||
throw std::invalid_argument("Scene path cannot be empty");
|
||||
}
|
||||
|
||||
if (value.find("://") != std::string_view::npos) {
|
||||
return AssetFS::GetInstance().GetFullPath(value);
|
||||
}
|
||||
|
||||
const std::filesystem::path path{value};
|
||||
return path.is_absolute() ? path : exeDir / path;
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::updateDebugUi()
|
||||
{
|
||||
if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_1))
|
||||
{
|
||||
renderer.setRenderWireframe(!renderer.getRenderWireframe());
|
||||
}
|
||||
|
||||
if (!m_params.showDebugUi) {
|
||||
return;
|
||||
}
|
||||
|
||||
drawDebugMenuBar();
|
||||
if (showTransformGizmo && !isPlayerViewActive()) {
|
||||
GameObject* selected = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject();
|
||||
if (selected != nullptr && !RectGizmo::CanManipulate(*selected)) {
|
||||
transformGizmo.HandleShortcuts();
|
||||
transformGizmo.DrawToolbar();
|
||||
}
|
||||
}
|
||||
if (showTestWindow) {
|
||||
drawTestWindow();
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::customWindowsMenu()
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::MenuItem("Test", nullptr, &showTestWindow);
|
||||
}
|
||||
|
||||
void LightKeeper::drawTestWindow()
|
||||
{
|
||||
ImGui::Begin("Test", &showTestWindow);
|
||||
if (ImGui::Button("Spawn ball"))
|
||||
{
|
||||
spawnBall();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void LightKeeper::spawnBall()
|
||||
{
|
||||
Scene& scene = SceneManager::GetInstance().GetCurrentScene();
|
||||
|
||||
GameObject* sphere = scene.CreateGameObject("Sphere");
|
||||
sphere->AddComponent<SphereCollider>(1.5f);
|
||||
sphere->AddComponent<Rigidbody>()->SetMass(1000);
|
||||
|
||||
auto* meshRenderComp = sphere->AddComponent<MeshRendererComponent>();
|
||||
meshRenderComp->SetMaterialID(mainScene.sphereMaterial);
|
||||
meshRenderComp->SetMeshID(mainScene.sphereMesh);
|
||||
|
||||
sphere->GetTransform().SetWorldPosition((std::rand() % 2) - 0.5f, 100, (std::rand() % 2) - 0.5f);
|
||||
sphere->GetTransform().SetWorldScale(glm::vec3{0.015f});
|
||||
|
||||
scene.GetPhysics().RefreshGameObject(*sphere);
|
||||
}
|
||||
|
||||
void LightKeeper::drawDebugMenuBar()
|
||||
{
|
||||
if (!ImGui::BeginMainMenuBar()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("File")) {
|
||||
ImGui::TextDisabled("Scene");
|
||||
|
||||
ImGui::SetNextItemWidth(320.0f);
|
||||
ImGui::InputText("Scene path", scenePath, sizeof(scenePath));
|
||||
|
||||
ImGui::SetNextItemWidth(320.0f);
|
||||
ImGui::InputText("New scene name", newSceneName, sizeof(newSceneName));
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem("New Empty Scene")) {
|
||||
createEmptyScene();
|
||||
}
|
||||
if (ImGui::MenuItem("Load")) {
|
||||
loadScene();
|
||||
}
|
||||
if (ImGui::MenuItem("Save")) {
|
||||
saveCurrentScene();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Prefab");
|
||||
|
||||
ImGui::SetNextItemWidth(320.0f);
|
||||
ImGui::InputText("Prefab path", prefabPath, sizeof(prefabPath));
|
||||
|
||||
const bool hasSelection = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject() != nullptr;
|
||||
if (ImGui::MenuItem("Save Selected as Prefab", nullptr, false, hasSelection)) {
|
||||
saveSelectedPrefab();
|
||||
}
|
||||
if (ImGui::MenuItem("Instantiate Prefab")) {
|
||||
instantiatePrefab();
|
||||
}
|
||||
|
||||
if (!sceneStatus.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", sceneStatus.c_str());
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Render")) {
|
||||
ImGui::MenuItem("Colliders", nullptr, &renderColliders);
|
||||
ImGui::MenuItem("2D Shapes", nullptr, &render2DShapes);
|
||||
ImGui::MenuItem("Debug Lines", nullptr, &renderDebugLines);
|
||||
ImGui::MenuItem("Transform Gizmo", nullptr, &showTransformGizmo);
|
||||
|
||||
bool lightHeatmap = renderer.getRenderLightHeatmap();
|
||||
if (ImGui::MenuItem("Forward+ Light Heatmap", nullptr, &lightHeatmap)) {
|
||||
renderer.setRenderLightHeatmap(lightHeatmap);
|
||||
}
|
||||
|
||||
bool selectionView = renderer.getRenderSelectionView();
|
||||
if (ImGui::MenuItem("Selection Buffer", nullptr, &selectionView)) {
|
||||
renderer.setRenderSelectionView(selectionView);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Camera")) {
|
||||
ImGui::MenuItem("Free Camera", nullptr, &useFreeCamera);
|
||||
ImGui::Separator();
|
||||
|
||||
auto& scene = SceneManager::GetInstance().GetCurrentScene();
|
||||
const auto cameras = scene.GetCameras();
|
||||
if (cameras.empty()) {
|
||||
ImGui::TextDisabled("No cameras in the scene");
|
||||
}
|
||||
for (CameraComponent* sceneCamera : cameras) {
|
||||
const GameObject* owner = sceneCamera->GetGameObject();
|
||||
ImGui::PushID(owner);
|
||||
if (ImGui::MenuItem(owner->GetName().c_str(), nullptr, !useFreeCamera && sceneCamera->IsCurrent())) {
|
||||
scene.SetCurrentCamera(sceneCamera);
|
||||
useFreeCamera = false;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
drawWindowMenu();
|
||||
|
||||
ImGui::EndMainMenuBar();
|
||||
}
|
||||
|
||||
void LightKeeper::onViewportAssetDropped(std::string_view path)
|
||||
{
|
||||
if (!AssetBrowser::IsModel(path)) {
|
||||
sceneStatus = "Only models can be dropped in the viewport: " + std::string{path};
|
||||
return;
|
||||
}
|
||||
|
||||
ModelDoc::LoadOptions options{};
|
||||
options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
|
||||
options.loadMaterials = true;
|
||||
|
||||
try {
|
||||
Scene& scene = SceneManager::GetInstance().GetCurrentScene();
|
||||
const std::string name = std::filesystem::path{path}.stem().string();
|
||||
GameObject* model = ModelSpawner::SpawnStaticModel(scene, gfxDevice, resources, path, options, name);
|
||||
|
||||
const Camera& view = getViewCamera();
|
||||
model->GetTransform().SetWorldPosition(view.GetPosition() + view.GetForward() * 5.0f);
|
||||
scene.SetSelectedObject(model->GetId());
|
||||
sceneStatus = "Placed " + std::string{path};
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::error("Could not place {}: {}", path, exception.what());
|
||||
sceneStatus = "Could not place " + std::string{path};
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::focusSelection()
|
||||
{
|
||||
GameObject* selected = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject();
|
||||
if (selected == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
glm::vec3 boundsMin{std::numeric_limits<float>::max()};
|
||||
glm::vec3 boundsMax{std::numeric_limits<float>::lowest()};
|
||||
std::vector<GameObject*> pending{selected};
|
||||
while (!pending.empty()) {
|
||||
GameObject* object = pending.back();
|
||||
pending.pop_back();
|
||||
for (const Transform* child : object->GetTransform().GetChildren()) {
|
||||
if (child != nullptr && child->GetOwner() != nullptr) {
|
||||
pending.push_back(child->GetOwner());
|
||||
}
|
||||
}
|
||||
|
||||
const auto* meshRenderer = object->GetComponent<MeshRendererComponent>();
|
||||
if (meshRenderer == nullptr || meshRenderer->GetMeshID() == NULL_MESH_ID) {
|
||||
continue;
|
||||
}
|
||||
const GPUMesh& mesh = resources.meshes().getMesh(meshRenderer->GetMeshID());
|
||||
const glm::mat4 world = object->GetTransform().GetWorldMatrix();
|
||||
for (int corner = 0; corner < 8; ++corner) {
|
||||
const glm::vec3 local{
|
||||
corner & 1 ? mesh.maxPos.x : mesh.minPos.x,
|
||||
corner & 2 ? mesh.maxPos.y : mesh.minPos.y,
|
||||
corner & 4 ? mesh.maxPos.z : mesh.minPos.z,
|
||||
};
|
||||
const glm::vec3 point{world * glm::vec4{local, 1.0f}};
|
||||
boundsMin = glm::min(boundsMin, point);
|
||||
boundsMax = glm::max(boundsMax, point);
|
||||
}
|
||||
}
|
||||
|
||||
const bool hasBounds = boundsMin.x <= boundsMax.x;
|
||||
const glm::vec3 center = hasBounds ? (boundsMin + boundsMax) * 0.5f : selected->GetTransform().GetWorldPosition();
|
||||
const float radius = hasBounds ? std::max(glm::length(boundsMax - boundsMin) * 0.5f, 0.1f) : 1.0f;
|
||||
|
||||
// Fit the bounding sphere in the narrower of the two field of view angles
|
||||
const float halfFovY = glm::radians(camera.GetFOVY()) * 0.5f;
|
||||
const float halfFovX = std::atan(std::tan(halfFovY) * camera.GetAspectRatio());
|
||||
const float distance = radius / std::sin(std::min(halfFovX, halfFovY)) * 1.1f;
|
||||
focusTarget = center - camera.GetForward() * distance;
|
||||
}
|
||||
|
||||
void LightKeeper::updateFocus(float dt)
|
||||
{
|
||||
if (!focusTarget) {
|
||||
return;
|
||||
}
|
||||
if (InputManager::GetInstance().IsMouseDown(SDL_BUTTON_RIGHT)) {
|
||||
focusTarget.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
camera.m_position = glm::mix(camera.m_position, *focusTarget, 1.0f - std::exp(-12.0f * dt));
|
||||
if (glm::distance(camera.m_position, *focusTarget) < 0.01f) {
|
||||
camera.m_position = *focusTarget;
|
||||
focusTarget.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::drawViewportOverlay()
|
||||
{
|
||||
if (!showTransformGizmo || isPlayerViewActive()) {
|
||||
return;
|
||||
}
|
||||
GameObject* selected = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject();
|
||||
if (selected == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ViewportRect& view = getViewportRect();
|
||||
if (RectGizmo::CanManipulate(*selected)) {
|
||||
const VkExtent3D extent = renderer.getDrawImage().extent;
|
||||
rectGizmo.Manipulate(*selected, glm::vec2{static_cast<float>(extent.width), static_cast<float>(extent.height)},
|
||||
view.min, view.size);
|
||||
if (rectGizmo.IsInteracting()) {
|
||||
claimViewportMouse();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
transformGizmo.Manipulate(*selected, getViewCamera(), view.min, view.size);
|
||||
if (transformGizmo.IsOver() || transformGizmo.IsUsing()) {
|
||||
claimViewportMouse();
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::drawWindowMenu()
|
||||
{
|
||||
if (!ImGui::BeginMenu("Window")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const WindowMode mode = window.GetMode();
|
||||
const bool fullscreen = window.IsFullscreen();
|
||||
const bool maximized = window.IsMaximized();
|
||||
|
||||
if (ImGui::MenuItem("Fullscreen", "F11", fullscreen)) {
|
||||
window.ToggleFullscreen();
|
||||
}
|
||||
if (ImGui::BeginMenu("Mode")) {
|
||||
if (ImGui::MenuItem("Windowed", nullptr, mode == WindowMode::Windowed)) {
|
||||
window.SetMode(WindowMode::Windowed);
|
||||
}
|
||||
if (ImGui::MenuItem("Borderless Fullscreen", nullptr, mode == WindowMode::BorderlessFullscreen)) {
|
||||
window.SetMode(WindowMode::BorderlessFullscreen);
|
||||
}
|
||||
if (ImGui::MenuItem("Exclusive Fullscreen", nullptr, mode == WindowMode::Fullscreen)) {
|
||||
window.SetMode(WindowMode::Fullscreen);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(maximized ? "Restore" : "Maximize", nullptr, false, !fullscreen)) {
|
||||
window.ToggleMaximized();
|
||||
}
|
||||
if (ImGui::MenuItem("Minimize", nullptr, false, !fullscreen)) {
|
||||
window.Minimize();
|
||||
}
|
||||
if (ImGui::MenuItem("Center", nullptr, false, !fullscreen && !maximized)) {
|
||||
window.Center();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Size", !fullscreen)) {
|
||||
const glm::ivec2 sizes[] = {glm::ivec2{1280, 720}, glm::ivec2{1600, 900}, glm::ivec2{1920, 1080}};
|
||||
for (const glm::ivec2& size : sizes) {
|
||||
const std::string label = std::to_string(size.x) + " x " + std::to_string(size.y);
|
||||
if (ImGui::MenuItem(label.c_str(), nullptr, window.GetSize() == size)) {
|
||||
if (maximized) {
|
||||
window.Restore();
|
||||
}
|
||||
window.SetSize(size);
|
||||
window.Center();
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (bool resizable = window.IsResizable(); ImGui::MenuItem("Resizable", nullptr, &resizable, !fullscreen)) {
|
||||
window.SetResizable(resizable);
|
||||
}
|
||||
if (bool bordered = window.IsBordered(); ImGui::MenuItem("Bordered", nullptr, &bordered, !fullscreen)) {
|
||||
window.SetBordered(bordered);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
const glm::ivec2 drawableSize = window.GetDrawableSize();
|
||||
ImGui::TextDisabled("Drawable: %d x %d", drawableSize.x, drawableSize.y);
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
void LightKeeper::saveCurrentScene()
|
||||
{
|
||||
try {
|
||||
const auto path = ResolveScenePath(scenePath, m_params.exeDir);
|
||||
const auto parent = path.parent_path();
|
||||
if (!parent.empty()) {
|
||||
std::filesystem::create_directories(parent);
|
||||
}
|
||||
|
||||
if (SceneSerializer::Save(
|
||||
SceneManager::GetInstance().GetCurrentScene(),
|
||||
path)) {
|
||||
sceneStatus = "Saved scene to " + path.string();
|
||||
} else {
|
||||
sceneStatus = "Failed to save scene to " + path.string();
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
sceneStatus = std::string{"Save failed: "} + exception.what();
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::saveSelectedPrefab()
|
||||
{
|
||||
try {
|
||||
const GameObject* selected = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject();
|
||||
if (selected == nullptr) {
|
||||
sceneStatus = "Select an object to save as a prefab";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto path = ResolveScenePath(prefabPath, m_params.exeDir);
|
||||
const auto parent = path.parent_path();
|
||||
if (!parent.empty()) {
|
||||
std::filesystem::create_directories(parent);
|
||||
}
|
||||
|
||||
sceneStatus = PrefabSerializer::Save(*selected, path)
|
||||
? "Saved prefab \"" + selected->GetName() + "\" to " + path.string()
|
||||
: "Failed to save prefab to " + path.string();
|
||||
} catch (const std::exception& exception) {
|
||||
sceneStatus = std::string{"Prefab save failed: "} + exception.what();
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::instantiatePrefab()
|
||||
{
|
||||
try {
|
||||
const auto path = ResolveScenePath(prefabPath, m_params.exeDir);
|
||||
const GameObject* instance = PrefabSerializer::Instantiate(
|
||||
SceneManager::GetInstance().GetCurrentScene(), path.string());
|
||||
sceneStatus = instance != nullptr
|
||||
? "Instantiated \"" + instance->GetName() + "\" from " + path.string()
|
||||
: "Failed to instantiate prefab " + path.string();
|
||||
} catch (const std::exception& exception) {
|
||||
sceneStatus = std::string{"Prefab instantiate failed: "} + exception.what();
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::loadScene()
|
||||
{
|
||||
try {
|
||||
const auto path = ResolveScenePath(scenePath, m_params.exeDir);
|
||||
if (SceneSerializer::Load(
|
||||
SceneManager::GetInstance().GetCurrentScene(),
|
||||
path)) {
|
||||
sceneStatus = "Loaded scene from " + path.string();
|
||||
} else {
|
||||
sceneStatus = "Failed to load scene from " + path.string();
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
sceneStatus = std::string{"Load failed: "} + exception.what();
|
||||
}
|
||||
findPlayer();
|
||||
}
|
||||
|
||||
void LightKeeper::createEmptyScene()
|
||||
{
|
||||
try {
|
||||
const std::string name = newSceneName[0] != '\0'
|
||||
? newSceneName
|
||||
: "EmptyScene";
|
||||
auto& sceneManager = SceneManager::GetInstance();
|
||||
const int newSceneIndex = sceneManager.GetSceneCount();
|
||||
sceneManager.CreateScene(name);
|
||||
sceneManager.SwitchScene(newSceneIndex);
|
||||
sceneStatus = "Created and switched to scene '" + name + "'";
|
||||
} catch (const std::exception& exception) {
|
||||
sceneStatus = std::string{"New scene failed: "} + exception.what();
|
||||
}
|
||||
findPlayer();
|
||||
}
|
||||
|
||||
// Loading or switching scenes replaces the player, so look it up again instead of keeping a dangling pointer
|
||||
void LightKeeper::findPlayer()
|
||||
{
|
||||
mainScene.player = nullptr;
|
||||
mainScene.playerController = nullptr;
|
||||
|
||||
auto& sceneManager = SceneManager::GetInstance();
|
||||
if (sceneManager.GetActiveScenes().empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& object : sceneManager.GetCurrentScene().GetObjects()) {
|
||||
if (object == nullptr || object->IsBeingDestroyed()) {
|
||||
continue;
|
||||
}
|
||||
if (auto* controller = object->GetComponent<FirstPersonController>()) {
|
||||
mainScene.player = object.get();
|
||||
mainScene.playerController = controller;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::submitDebugDraws()
|
||||
{
|
||||
submitColliderDebugLines();
|
||||
|
||||
if (renderDebugLines) {
|
||||
LineRenderingManager::GetInstance().SubmitLine({0, 0, 0}, {0, 10, 0}, {255, 0, 0, 255});
|
||||
}
|
||||
}
|
||||
|
||||
// Triggers orange, then colored by body type: static blue, dynamic green, kinematic purple
|
||||
void LightKeeper::submitColliderDebugLines()
|
||||
{
|
||||
if (!renderColliders) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& sceneManager = SceneManager::GetInstance();
|
||||
if (sceneManager.GetActiveScenes().empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& object : sceneManager.GetCurrentScene().GetObjects()) {
|
||||
if (object == nullptr || object->IsBeingDestroyed() || !object->IsActiveInHierarchy()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto* rigidbody = object->GetComponent<Rigidbody>();
|
||||
const RigidbodyType type = rigidbody != nullptr ? rigidbody->GetType() : RigidbodyType::Static;
|
||||
|
||||
for (const auto& component : object->GetComponents()) {
|
||||
const auto* collider = dynamic_cast<const Collider*>(component.get());
|
||||
if (collider == nullptr || collider->IsBeingDestroyed() || !collider->isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
glm::vec4 color{0.1f, 0.8f, 1.0f, 1.0f};
|
||||
if (collider->IsTrigger()) {
|
||||
color = {1.0f, 0.75f, 0.1f, 1.0f};
|
||||
} else if (type == RigidbodyType::Dynamic) {
|
||||
color = {0.3f, 1.0f, 0.3f, 1.0f};
|
||||
} else if (type == RigidbodyType::Kinematic) {
|
||||
color = {0.8f, 0.4f, 1.0f, 1.0f};
|
||||
}
|
||||
collider->DrawDebugLines(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,26 @@
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <SDL.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <editor/Editor.h>
|
||||
|
||||
#include "Lightkeeper.h"
|
||||
#include "spdlog/spdlog.h"
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", SDL_GetError(), nullptr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::span<char*> arguments{argv, static_cast<std::size_t>(argc)};
|
||||
const bool runGameOnly = std::ranges::any_of(arguments, [](const char* argument) { return std::string_view{argument} == "--game"; });
|
||||
|
||||
try {
|
||||
char* basePath = SDL_GetBasePath();
|
||||
if (basePath == nullptr) {
|
||||
@@ -28,8 +38,13 @@ int main(int argc, char* argv[]) {
|
||||
.appName = "Destrum Engine",
|
||||
.windowTitle = "Lightkeeper",
|
||||
.exeDir = exeDir,
|
||||
.startInEditMode = true,
|
||||
});
|
||||
if (!runGameOnly) {
|
||||
app.attachLayer(std::make_unique<Editor>(app, EditorSettings{
|
||||
.gameAssetsSource = LIGHTKEEPER_ASSETS_SRC_DIR,
|
||||
.scenePath = std::string{LightKeeper::MainScenePath},
|
||||
}));
|
||||
}
|
||||
app.run();
|
||||
app.cleanup();
|
||||
} catch (const std::exception& exception) {
|
||||
@@ -41,4 +56,3 @@ int main(int argc, char* argv[]) {
|
||||
SDL_Quit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,792 +0,0 @@
|
||||
#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/Components/UI/Canvas.h>
|
||||
#include <destrum/Components/UI/RectTransform.h>
|
||||
#include <destrum/Components/UI/UIImage.h>
|
||||
#include <destrum/Components/UI/UIText.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/Checkpoint.h>
|
||||
#include <components/CrosshairUI.h>
|
||||
#include <components/Door.h>
|
||||
#include <components/InteractableButton.h>
|
||||
#include <components/LightToggle.h>
|
||||
#include <components/MovingPlatform.h>
|
||||
#include <components/PlayerRespawn.h>
|
||||
#include <components/TriggerLoggerComponent.h>
|
||||
#include <components/WinGoal.h>
|
||||
#include <components/WinScreenUI.h>
|
||||
|
||||
#include <glm/gtc/constants.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
// The visible ground plane is at y = -1
|
||||
constexpr float GroundHeight = -1.0f;
|
||||
constexpr float GroundHalfExtent = 25.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 CreateHelmetSpotlight(Scene& scene)
|
||||
{
|
||||
const glm::vec3 position{2.0f, 3.5f, -2.5f};
|
||||
const glm::vec3 target{2.0f, GroundHeight, 0.0f};
|
||||
|
||||
GameObject* spotObj = scene.CreateGameObject("HelmetSpotlight");
|
||||
spotObj->GetTransform().SetWorldPosition(position);
|
||||
spotObj->GetTransform().SetWorldRotation(glm::quatLookAt(glm::normalize(target - position), glm::vec3{0.0f, 1.0f, 0.0f}));
|
||||
|
||||
auto* spot = spotObj->AddComponent<LightComponent>(LightType::Spot);
|
||||
spot->SetColor(glm::vec3{1.0f, 0.85f, 0.7f});
|
||||
spot->SetIntensity(25.0f);
|
||||
spot->SetRange(12.0f);
|
||||
spot->SetConeAngles(glm::radians(20.0f), glm::radians(30.0f));
|
||||
spot->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(GroundHalfExtent, 1.f, GroundHalfExtent));
|
||||
|
||||
// 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{GroundHalfExtent, 0.5f, GroundHalfExtent});
|
||||
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,
|
||||
const glm::quat& rotation = glm::quat{1.0f, 0.0f, 0.0f, 0.0f})
|
||||
{
|
||||
GameObject* box = scene.CreateGameObject(name);
|
||||
box->GetTransform().SetWorldPosition(position);
|
||||
box->GetTransform().SetWorldRotation(rotation);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
GameObject* CreateDoorway(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
|
||||
{
|
||||
const MaterialID frameMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f);
|
||||
const MaterialID doorMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.5f);
|
||||
|
||||
constexpr float x = 4.0f;
|
||||
constexpr float z = -4.5f;
|
||||
constexpr float frameHeight = 2.8f;
|
||||
constexpr float doorWidth = 1.6f;
|
||||
constexpr float doorHeight = 2.4f;
|
||||
|
||||
for (const float side : {-1.0f, 1.0f}) {
|
||||
CreateBox(scene, gfxDevice, resources, side < 0.0f ? "DoorFrame_Left" : "DoorFrame_Right",
|
||||
{x, GroundHeight + frameHeight * 0.5f, z + side * (doorWidth * 0.5f + 0.2f)},
|
||||
{0.4f, frameHeight, 0.4f}, frameMaterial, RigidbodyType::Static);
|
||||
}
|
||||
CreateBox(scene, gfxDevice, resources, "DoorFrame_Top", {x, GroundHeight + frameHeight + 0.2f, z},
|
||||
{0.4f, 0.4f, doorWidth + 0.8f}, frameMaterial, RigidbodyType::Static);
|
||||
|
||||
GameObject* door = CreateBox(scene, gfxDevice, resources, "Door", {x, GroundHeight + doorHeight * 0.5f, z},
|
||||
{0.3f, doorHeight, doorWidth}, doorMaterial, RigidbodyType::Kinematic);
|
||||
door->AddComponent<Door>()->SetOpenOffset({0.0f, 0.0f, -doorWidth - 0.1f});
|
||||
return door;
|
||||
}
|
||||
|
||||
void CreateButton(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources, const GameObject& target)
|
||||
{
|
||||
const MaterialID pedestalMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f);
|
||||
CreateBox(scene, gfxDevice, resources, "ButtonPedestal", {1.5f, -0.45f, -4.5f}, {0.4f, 1.1f, 0.4f},
|
||||
pedestalMaterial, RigidbodyType::Static);
|
||||
|
||||
// Every button needs its own material, pressing it recolors the material
|
||||
Material buttonMaterial = Material::SimpleColor(glm::vec3{1.0f}, "ButtonMaterial");
|
||||
buttonMaterial.roughnessFactor = 0.4f;
|
||||
const MaterialID buttonMaterialID = resources.materials().addMaterial(std::move(buttonMaterial));
|
||||
|
||||
GameObject* button = CreateBox(scene, gfxDevice, resources, "Button", {1.5f, 0.15f, -4.5f}, {0.25f, 0.1f, 0.25f},
|
||||
buttonMaterialID, RigidbodyType::Static);
|
||||
button->AddComponent<InteractableButton>()->AddTarget(target);
|
||||
}
|
||||
|
||||
void CreateMovingPlatform(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
|
||||
{
|
||||
const MaterialID rampMaterial = AddPrototypeMaterial(gfxDevice, resources, "Green", 0.6f);
|
||||
const MaterialID platformMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.5f);
|
||||
|
||||
constexpr float laneZ = 2.0f;
|
||||
constexpr float platformHeight = 1.0f;
|
||||
constexpr float rampStartX = 9.0f;
|
||||
constexpr float rampLength = 3.0f;
|
||||
constexpr float rampThickness = 0.3f;
|
||||
constexpr float travelDistance = 8.0f;
|
||||
const glm::vec3 platformSize{3.0f, 0.3f, 3.0f};
|
||||
|
||||
const float platformTop = GroundHeight + platformHeight;
|
||||
const glm::quat rampRotation =
|
||||
glm::angleAxis(std::atan2(platformHeight, rampLength), glm::vec3{0.0f, 0.0f, 1.0f});
|
||||
const glm::vec3 rampTopCenter{rampStartX + rampLength * 0.5f, GroundHeight + platformHeight * 0.5f, laneZ};
|
||||
const glm::vec3 rampCenter = rampTopCenter - rampRotation * glm::vec3{0.0f, rampThickness * 0.5f, 0.0f};
|
||||
const float rampSlopeLength = glm::length(glm::vec2{rampLength, platformHeight});
|
||||
|
||||
CreateBox(scene, gfxDevice, resources, "PlatformRamp", rampCenter,
|
||||
{rampSlopeLength, rampThickness, platformSize.z}, rampMaterial, RigidbodyType::Static,
|
||||
rampRotation);
|
||||
|
||||
const float platformStartX = rampStartX + rampLength + platformSize.x * 0.5f;
|
||||
GameObject* platform = CreateBox(scene, gfxDevice, resources, "MovingPlatform",
|
||||
{platformStartX, platformTop - platformSize.y * 0.5f, laneZ},
|
||||
platformSize, platformMaterial, RigidbodyType::Kinematic);
|
||||
platform->AddComponent<MovingPlatform>()->SetTravelOffset({travelDistance, 0.0f, 0.0f});
|
||||
|
||||
const float landingStartX = platformStartX + travelDistance + platformSize.x * 0.5f;
|
||||
CreateBox(scene, gfxDevice, resources, "PlatformLanding",
|
||||
{landingStartX + 1.0f, GroundHeight + platformHeight * 0.5f, laneZ},
|
||||
{2.0f, platformHeight, platformSize.z}, rampMaterial, RigidbodyType::Static);
|
||||
}
|
||||
|
||||
// Floats over the void past the west edge of the ground. The controller jumps 1.3 m and sprints 8.5 m/s,
|
||||
// which carries about 5 m on a level jump, the hardest gaps stay a little under that
|
||||
WinGoal* CreateWinCourse(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
|
||||
{
|
||||
const MaterialID checkpointMaterial = AddPrototypeMaterial(gfxDevice, resources, "Green", 0.6f);
|
||||
const MaterialID pillarMaterial = AddPrototypeMaterial(gfxDevice, resources, "Orange", 0.6f);
|
||||
const MaterialID stoneMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.5f);
|
||||
const MaterialID beamMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f);
|
||||
const MaterialID movingMaterial = AddPrototypeMaterial(gfxDevice, resources, "Light", 0.5f);
|
||||
const MaterialID goalMaterial = AddPrototypeMaterial(gfxDevice, resources, "Red", 0.6f);
|
||||
|
||||
constexpr float slabThickness = 0.5f;
|
||||
|
||||
GameObject* course = scene.CreateGameObject("WinCourse");
|
||||
course->GetTransform().SetWorldPosition({-GroundHalfExtent, GroundHeight, 0.0f});
|
||||
const auto addToCourse = [&](GameObject* object)
|
||||
{
|
||||
object->GetTransform().SetParent(&course->GetTransform(), true);
|
||||
return object;
|
||||
};
|
||||
|
||||
// Top center and half size of the last placed platform, the course runs along -X
|
||||
glm::vec3 top{-GroundHalfExtent - 2.5f, GroundHeight, 0.0f};
|
||||
glm::vec2 half{2.5f, 2.5f};
|
||||
int platformCount = 0;
|
||||
int checkpointCount = 0;
|
||||
|
||||
const auto slab = [&](const std::string& name, const glm::vec3& topCenter, const glm::vec2& size,
|
||||
MaterialID materialID, RigidbodyType type)
|
||||
{
|
||||
return addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_" + name,
|
||||
topCenter - glm::vec3{0.0f, slabThickness * 0.5f, 0.0f},
|
||||
{size.x, slabThickness, size.y}, materialID, type));
|
||||
};
|
||||
|
||||
// gap is the edge to edge distance along X, rise and dz move the top center
|
||||
const auto next = [&](float gap, float rise, float dz, const glm::vec2& size, MaterialID materialID,
|
||||
RigidbodyType type = RigidbodyType::Static)
|
||||
{
|
||||
top = glm::vec3{top.x - half.x - gap - size.x * 0.5f, top.y + rise, top.z + dz};
|
||||
half = size * 0.5f;
|
||||
return slab("Platform" + std::to_string(++platformCount), top, size, materialID, type);
|
||||
};
|
||||
|
||||
const auto mover = [&](float gap, float rise, float dz, const glm::vec2& size, const glm::vec3& travel,
|
||||
float travelDuration, float pauseDuration)
|
||||
{
|
||||
auto* platform = next(gap, rise, dz, size, movingMaterial, RigidbodyType::Kinematic)
|
||||
->AddComponent<MovingPlatform>();
|
||||
platform->SetTravelOffset(travel);
|
||||
platform->SetTravelDuration(travelDuration);
|
||||
platform->SetPauseDuration(pauseDuration);
|
||||
};
|
||||
|
||||
const auto addCheckpoint = [&]()
|
||||
{
|
||||
GameObject* trigger = addToCourse(
|
||||
scene.CreateGameObject("WinCourse_Checkpoint" + std::to_string(++checkpointCount)));
|
||||
trigger->GetTransform().SetWorldPosition(top + glm::vec3{0.0f, 1.0f, 0.0f});
|
||||
trigger->AddComponent<BoxCollider>(glm::vec3{half.x, 1.0f, half.y})->SetTrigger(true);
|
||||
trigger->AddComponent<Rigidbody>()->SetType(RigidbodyType::Static);
|
||||
trigger->AddComponent<Checkpoint>()->SetSpawnOffset({0.0f, 0.2f, 0.0f});
|
||||
scene.GetPhysics().RegisterGameObject(*trigger);
|
||||
};
|
||||
|
||||
// The start pad touches the ground edge so it can be walked onto
|
||||
slab("Start", top, {5.0f, 5.0f}, checkpointMaterial, RigidbodyType::Static);
|
||||
addCheckpoint();
|
||||
|
||||
// Warm-up hops
|
||||
next(2.0f, 0.0f, 0.0f, {2.0f, 2.0f}, pillarMaterial);
|
||||
next(2.4f, 0.5f, 1.5f, {1.5f, 1.5f}, pillarMaterial);
|
||||
next(2.6f, 0.5f, -3.0f, {1.5f, 1.5f}, pillarMaterial);
|
||||
next(2.8f, 0.0f, 1.5f, {1.2f, 1.2f}, pillarMaterial);
|
||||
next(2.2f, -0.5f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
|
||||
addCheckpoint();
|
||||
|
||||
// Zig-zag over shrinking stones
|
||||
next(2.6f, 0.0f, 1.0f, {1.0f, 1.0f}, stoneMaterial);
|
||||
next(2.8f, 0.4f, -2.0f, {0.8f, 0.8f}, stoneMaterial);
|
||||
next(2.8f, 0.4f, 2.0f, {0.7f, 0.7f}, stoneMaterial);
|
||||
next(3.0f, 0.4f, -2.0f, {0.7f, 0.7f}, stoneMaterial);
|
||||
next(3.2f, -0.8f, 1.0f, {0.7f, 0.7f}, stoneMaterial);
|
||||
next(2.5f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
|
||||
addCheckpoint();
|
||||
|
||||
// Offset balance beams, then long sprint jumps
|
||||
next(1.2f, 0.0f, 0.0f, {6.0f, 0.4f}, beamMaterial);
|
||||
next(1.4f, 0.3f, 0.9f, {5.0f, 0.35f}, beamMaterial);
|
||||
next(1.4f, 0.3f, -1.8f, {4.0f, 0.3f}, beamMaterial);
|
||||
next(3.6f, 0.0f, 0.9f, {1.5f, 1.5f}, pillarMaterial);
|
||||
next(4.0f, -0.6f, 0.0f, {1.5f, 1.5f}, pillarMaterial);
|
||||
next(3.8f, 0.0f, 0.0f, {1.2f, 1.2f}, pillarMaterial);
|
||||
next(3.0f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
|
||||
addCheckpoint();
|
||||
|
||||
// A ferry over a 9 m hole, then sideways movers that drift out of sync
|
||||
constexpr float ferryTravel = 9.0f;
|
||||
mover(0.6f, 0.0f, 0.0f, {2.5f, 2.5f}, {-ferryTravel, 0.0f, 0.0f}, 3.5f, 1.0f);
|
||||
top.x -= ferryTravel;
|
||||
next(0.6f, 0.0f, 0.0f, {2.0f, 2.0f}, pillarMaterial);
|
||||
|
||||
constexpr float sway = 3.0f;
|
||||
mover(2.2f, 0.0f, -sway, {1.5f, 1.5f}, {0.0f, 0.0f, sway * 2.0f}, 2.0f, 0.3f);
|
||||
top.z += sway;
|
||||
mover(2.2f, 0.0f, sway, {1.5f, 1.5f}, {0.0f, 0.0f, -sway * 2.0f}, 2.6f, 0.3f);
|
||||
top.z -= sway;
|
||||
mover(2.2f, 0.0f, -sway, {1.5f, 1.5f}, {0.0f, 0.0f, sway * 2.0f}, 1.7f, 0.3f);
|
||||
top.z += sway;
|
||||
next(2.2f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
|
||||
addCheckpoint();
|
||||
|
||||
// An elevator up to the last stretch
|
||||
constexpr float liftHeight = 5.0f;
|
||||
mover(0.6f, 0.0f, 0.0f, {2.0f, 2.0f}, {0.0f, liftHeight, 0.0f}, 3.0f, 1.5f);
|
||||
top.y += liftHeight;
|
||||
next(1.2f, 0.0f, 0.0f, {1.5f, 1.5f}, pillarMaterial);
|
||||
|
||||
next(2.8f, 0.5f, 1.2f, {0.8f, 0.8f}, stoneMaterial);
|
||||
next(3.0f, 0.5f, -2.4f, {0.7f, 0.7f}, stoneMaterial);
|
||||
next(3.2f, 0.0f, 2.4f, {0.6f, 0.6f}, stoneMaterial);
|
||||
next(3.4f, -0.4f, -2.4f, {0.6f, 0.6f}, stoneMaterial);
|
||||
next(2.5f, 0.0f, 1.2f, {3.0f, 3.0f}, checkpointMaterial);
|
||||
addCheckpoint();
|
||||
|
||||
// Final gauntlet: a fast sideways mover and three tiny stones
|
||||
constexpr float fastSway = 2.5f;
|
||||
mover(2.4f, 0.0f, -fastSway, {1.2f, 1.2f}, {0.0f, 0.0f, fastSway * 2.0f}, 1.6f, 0.2f);
|
||||
top.z += fastSway;
|
||||
next(2.6f, 0.8f, 0.0f, {1.0f, 1.0f}, stoneMaterial);
|
||||
next(3.4f, 0.0f, 1.0f, {0.8f, 0.8f}, stoneMaterial);
|
||||
next(3.6f, -0.3f, -2.0f, {0.8f, 0.8f}, stoneMaterial);
|
||||
|
||||
GameObject* goal = next(2.5f, 0.5f, 1.0f, {4.0f, 4.0f}, goalMaterial);
|
||||
goal->SetName("WinCourse_Goal");
|
||||
auto* winGoal = goal->AddComponent<WinGoal>();
|
||||
|
||||
const glm::vec3 buttonBase = top + glm::vec3{-1.0f, 0.0f, 0.0f};
|
||||
addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_ButtonPedestal",
|
||||
buttonBase + glm::vec3{0.0f, 0.55f, 0.0f}, {0.4f, 1.1f, 0.4f},
|
||||
beamMaterial, RigidbodyType::Static));
|
||||
|
||||
// Every button needs its own material, pressing it recolors the material
|
||||
Material buttonMaterial = Material::SimpleColor(glm::vec3{1.0f, 0.85f, 0.2f}, "WinButtonMaterial");
|
||||
buttonMaterial.roughnessFactor = 0.4f;
|
||||
GameObject* button = addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_Button",
|
||||
buttonBase + glm::vec3{0.0f, 1.15f, 0.0f}, {0.25f, 0.1f, 0.25f},
|
||||
resources.materials().addMaterial(std::move(buttonMaterial)),
|
||||
RigidbodyType::Static));
|
||||
button->AddComponent<InteractableButton>()->AddTarget(*goal);
|
||||
|
||||
GameObject* goalLight = addToCourse(scene.CreateGameObject("WinCourse_GoalLight"));
|
||||
goalLight->GetTransform().SetWorldPosition(top + glm::vec3{0.0f, 3.0f, 0.0f});
|
||||
auto* light = goalLight->AddComponent<LightComponent>(LightType::Point);
|
||||
light->SetColor(glm::vec3{1.0f, 0.8f, 0.3f});
|
||||
light->SetIntensity(6.0f);
|
||||
light->SetRange(8.0f);
|
||||
|
||||
return winGoal;
|
||||
}
|
||||
|
||||
struct WallOpening {
|
||||
float offset;
|
||||
float width;
|
||||
float bottom;
|
||||
float top;
|
||||
};
|
||||
|
||||
std::vector<GameObject*> CreateWall(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources, const std::string& name,
|
||||
const glm::vec3& floorCenter, float length, float height, bool alongX, MaterialID materialID,
|
||||
const std::optional<WallOpening>& opening = std::nullopt)
|
||||
{
|
||||
constexpr float thickness = 0.3f;
|
||||
std::vector<GameObject*> pieces;
|
||||
|
||||
const auto createPiece = [&](const std::string& suffix, float from, float to, float bottom, float top)
|
||||
{
|
||||
if (to <= from || top <= bottom) {
|
||||
return;
|
||||
}
|
||||
const float along = (from + to) * 0.5f;
|
||||
const glm::vec3 alongOffset = alongX ? glm::vec3{along, 0.0f, 0.0f} : glm::vec3{0.0f, 0.0f, along};
|
||||
const glm::vec3 center = floorCenter + alongOffset + glm::vec3{0.0f, (bottom + top) * 0.5f, 0.0f};
|
||||
const glm::vec3 size = alongX
|
||||
? glm::vec3{to - from, top - bottom, thickness}
|
||||
: glm::vec3{thickness, top - bottom, to - from};
|
||||
pieces.push_back(
|
||||
CreateBox(scene, gfxDevice, resources, name + suffix, center, size, materialID, RigidbodyType::Static));
|
||||
};
|
||||
|
||||
const float halfLength = length * 0.5f;
|
||||
if (!opening) {
|
||||
createPiece("", -halfLength, halfLength, 0.0f, height);
|
||||
return pieces;
|
||||
}
|
||||
|
||||
const float openingStart = opening->offset - opening->width * 0.5f;
|
||||
const float openingEnd = opening->offset + opening->width * 0.5f;
|
||||
createPiece("_Left", -halfLength, openingStart, 0.0f, height);
|
||||
createPiece("_Right", openingEnd, halfLength, 0.0f, height);
|
||||
createPiece("_Below", openingStart, openingEnd, 0.0f, opening->bottom);
|
||||
createPiece("_Above", openingStart, openingEnd, opening->top, height);
|
||||
return pieces;
|
||||
}
|
||||
|
||||
GameObject* CreateCeilingLight(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources,
|
||||
const glm::vec3& position)
|
||||
{
|
||||
GameObject* lightObj = scene.CreateGameObject("CeilingLight");
|
||||
lightObj->GetTransform().SetWorldPosition(position);
|
||||
|
||||
// Every bulb needs its own material, the toggle changes its glow
|
||||
Material bulbMaterial = Material::SimpleColor(glm::vec3{1.0f, 0.95f, 0.85f}, "BulbMaterial");
|
||||
auto* bulbMesh = lightObj->AddComponent<MeshRendererComponent>();
|
||||
bulbMesh->SetMeshID(resources.meshes().addMesh(gfxDevice, PrimitiveMeshes::MakeBox({0.35f, 0.12f, 0.35f})));
|
||||
bulbMesh->SetMaterialID(resources.materials().addMaterial(std::move(bulbMaterial)));
|
||||
|
||||
auto* light = lightObj->AddComponent<LightComponent>(LightType::Point);
|
||||
light->SetColor(glm::vec3{1.0f, 0.82f, 0.55f});
|
||||
light->SetIntensity(4.0f);
|
||||
light->SetRange(7.0f);
|
||||
|
||||
lightObj->AddComponent<LightToggle>();
|
||||
return lightObj;
|
||||
}
|
||||
|
||||
void CreateHouse(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
|
||||
{
|
||||
const MaterialID wallMaterial = AddPrototypeMaterial(gfxDevice, resources, "Light", 0.8f);
|
||||
const MaterialID floorMaterial = AddPrototypeMaterial(gfxDevice, resources, "Orange", 0.7f);
|
||||
const MaterialID roofMaterial = AddPrototypeMaterial(gfxDevice, resources, "Red", 0.8f);
|
||||
|
||||
const glm::vec3 center{0.0f, GroundHeight, -16.0f};
|
||||
constexpr float width = 8.0f;
|
||||
constexpr float depth = 6.0f;
|
||||
constexpr float wallHeight = 3.0f;
|
||||
constexpr float doorOffset = -1.5f;
|
||||
constexpr float doorWidth = 1.2f;
|
||||
|
||||
GameObject* house = scene.CreateGameObject("House");
|
||||
house->GetTransform().SetWorldPosition(center);
|
||||
const auto addToHouse = [&](GameObject* object)
|
||||
{
|
||||
object->GetTransform().SetParent(&house->GetTransform(), true);
|
||||
};
|
||||
const auto addWallToHouse = [&](const std::vector<GameObject*>& pieces)
|
||||
{
|
||||
for (GameObject* piece : pieces) {
|
||||
addToHouse(piece);
|
||||
}
|
||||
};
|
||||
|
||||
addToHouse(CreateBox(scene, gfxDevice, resources, "House_Floor", center + glm::vec3{0.0f, 0.03f, 0.0f},
|
||||
{width, 0.06f, depth}, floorMaterial, RigidbodyType::Static));
|
||||
|
||||
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_FrontWall", center + glm::vec3{0.0f, 0.0f, depth * 0.5f},
|
||||
width + 0.3f, wallHeight, true, wallMaterial,
|
||||
WallOpening{.offset = doorOffset, .width = doorWidth, .bottom = 0.0f, .top = 2.2f}));
|
||||
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_BackWall", center - glm::vec3{0.0f, 0.0f, depth * 0.5f},
|
||||
width + 0.3f, wallHeight, true, wallMaterial,
|
||||
WallOpening{.offset = 1.5f, .width = 1.4f, .bottom = 1.0f, .top = 2.0f}));
|
||||
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_LeftWall", center - glm::vec3{width * 0.5f, 0.0f, 0.0f},
|
||||
depth - 0.3f, wallHeight, false, wallMaterial));
|
||||
addWallToHouse(CreateWall(scene, gfxDevice, resources, "House_RightWall", center + glm::vec3{width * 0.5f, 0.0f, 0.0f},
|
||||
depth - 0.3f, wallHeight, false, wallMaterial,
|
||||
WallOpening{.offset = 0.0f, .width = 1.6f, .bottom = 1.0f, .top = 2.0f}));
|
||||
|
||||
addToHouse(CreateBox(scene, gfxDevice, resources, "House_Roof", center + glm::vec3{0.0f, wallHeight + 0.15f, 0.0f},
|
||||
{width + 0.8f, 0.3f, depth + 0.8f}, roofMaterial, RigidbodyType::Static));
|
||||
|
||||
GameObject* ceilingLight =
|
||||
CreateCeilingLight(scene, gfxDevice, resources, center + glm::vec3{0.0f, wallHeight - 0.06f, 0.0f});
|
||||
addToHouse(ceilingLight);
|
||||
|
||||
// Inside, next to the door
|
||||
Material switchMaterial = Material::SimpleColor(glm::vec3{0.9f}, "LightSwitchMaterial");
|
||||
switchMaterial.roughnessFactor = 0.5f;
|
||||
const glm::vec3 switchPosition =
|
||||
center + glm::vec3{doorOffset + doorWidth * 0.5f + 0.4f, 1.3f, depth * 0.5f - 0.15f - 0.03f};
|
||||
GameObject* lightSwitch = CreateBox(scene, gfxDevice, resources, "LightSwitch", switchPosition,
|
||||
{0.12f, 0.2f, 0.06f},
|
||||
resources.materials().addMaterial(std::move(switchMaterial)),
|
||||
RigidbodyType::Static);
|
||||
auto* switchButton = lightSwitch->AddComponent<InteractableButton>();
|
||||
switchButton->SetCycleColors(false);
|
||||
switchButton->AddTarget(*ceilingLight);
|
||||
addToHouse(lightSwitch);
|
||||
|
||||
GameObject* sofa = ModelSpawner::SpawnStaticModel(
|
||||
scene, gfxDevice, resources, "game://sofa.glb", StaticModelOptions(),
|
||||
"Sofa"
|
||||
);
|
||||
sofa->GetTransform().SetWorldPosition({-3.7, -1, -17.5});
|
||||
sofa->GetTransform().SetWorldRotation({180, 0, 180});
|
||||
sofa->GetTransform().SetWorldScale({3, 3, 3});
|
||||
addToHouse(sofa);
|
||||
}
|
||||
|
||||
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>());
|
||||
playerObj->AddComponent<PlayerRespawn>();
|
||||
scene.GetPhysics().RegisterGameObject(*playerObj);
|
||||
|
||||
objects.player = playerObj;
|
||||
objects.playerController = controller;
|
||||
}
|
||||
|
||||
// Fills its parent until it is given anchors or a size
|
||||
RectTransform* CreateUIObject(Scene& scene, const std::string& name, GameObject& parent)
|
||||
{
|
||||
GameObject* object = scene.CreateGameObject(name);
|
||||
object->GetTransform().SetParent(&parent.GetTransform(), false);
|
||||
auto* rect = object->AddComponent<RectTransform>();
|
||||
rect->Stretch();
|
||||
return rect;
|
||||
}
|
||||
|
||||
GameObject* CreateCrosshair(Scene& scene, GameObject& hud)
|
||||
{
|
||||
auto* crosshair = CreateUIObject(scene, "Crosshair", hud);
|
||||
crosshair->SetAnchor({0.5f, 0.5f});
|
||||
crosshair->SetSize({10.0f, 10.0f});
|
||||
|
||||
constexpr float thickness = 2.0f;
|
||||
auto* horizontal = CreateUIObject(scene, "Crosshair_Horizontal", *crosshair->GetGameObject());
|
||||
horizontal->SetAnchors({0.0f, 0.5f}, {1.0f, 0.5f});
|
||||
horizontal->SetSize({0.0f, thickness});
|
||||
horizontal->GetGameObject()->AddComponent<UIImage>();
|
||||
|
||||
auto* vertical = CreateUIObject(scene, "Crosshair_Vertical", *crosshair->GetGameObject());
|
||||
vertical->SetAnchors({0.5f, 0.0f}, {0.5f, 1.0f});
|
||||
vertical->SetSize({thickness, 0.0f});
|
||||
vertical->GetGameObject()->AddComponent<UIImage>();
|
||||
|
||||
return crosshair->GetGameObject();
|
||||
}
|
||||
|
||||
GameObject* CreateFocusIndicator(Scene& scene, GameObject& crosshair)
|
||||
{
|
||||
GameObject* indicator = CreateUIObject(scene, "FocusIndicator", crosshair)->GetGameObject();
|
||||
|
||||
auto* ring = CreateUIObject(scene, "FocusRing", *indicator);
|
||||
ring->SetAnchor({0.5f, 0.5f});
|
||||
ring->SetSize({28.0f, 28.0f});
|
||||
auto* ringImage = ring->GetGameObject()->AddComponent<UIImage>();
|
||||
ringImage->SetShape(UIImageShape::Ellipse);
|
||||
ringImage->SetOutline(2.0f);
|
||||
|
||||
auto* prompt = CreateUIObject(scene, "InteractPrompt", *indicator);
|
||||
prompt->SetAnchor({0.5f, 0.5f});
|
||||
prompt->SetPivot({0.5f, 0.0f});
|
||||
prompt->SetPosition({0.0f, 28.0f});
|
||||
prompt->SetSize({400.0f, 30.0f});
|
||||
auto* promptText = prompt->GetGameObject()->AddComponent<UIText>("[E] Interact", 22.0f);
|
||||
promptText->SetOutline(2.0f, {0.0f, 0.0f, 0.0f, 0.75f});
|
||||
promptText->SetAlignment(UITextAlign::Center, UITextAlign::Start);
|
||||
|
||||
return indicator;
|
||||
}
|
||||
|
||||
GameObject* CreateWinScreen(Scene& scene, GameObject& hud)
|
||||
{
|
||||
GameObject* screen = CreateUIObject(scene, "WinScreen", hud)->GetGameObject();
|
||||
screen->AddComponent<UIImage>(glm::vec4{0.0f, 0.0f, 0.0f, 0.35f});
|
||||
|
||||
auto* titleText = CreateUIObject(scene, "WinScreen_Title", *screen)->GetGameObject()->AddComponent<UIText>("YOU WIN", 195.0f);
|
||||
titleText->SetColor({1.0f, 0.85f, 0.2f, 1.0f});
|
||||
titleText->SetOutline(4.0f, {0.0f, 0.0f, 0.0f, 0.9f});
|
||||
|
||||
screen->SetActive(false);
|
||||
return screen;
|
||||
}
|
||||
|
||||
// Sizes are in 1080p pixels, the canvas scales them with the screen height
|
||||
void CreateHud(Scene& scene, const GameObject& player, const GameObject& winGoal)
|
||||
{
|
||||
GameObject* hud = scene.CreateGameObject("HUD");
|
||||
hud->AddComponent<Canvas>();
|
||||
|
||||
GameObject* crosshair = CreateCrosshair(scene, *hud);
|
||||
const GameObject* focusIndicator = CreateFocusIndicator(scene, *crosshair);
|
||||
auto* crosshairUi = hud->AddComponent<CrosshairUI>();
|
||||
crosshairUi->SetPlayer(player);
|
||||
crosshairUi->SetCrosshair(*crosshair);
|
||||
crosshairUi->SetFocusIndicator(*focusIndicator);
|
||||
|
||||
auto* winScreenUi = hud->AddComponent<WinScreenUI>();
|
||||
winScreenUi->SetGoal(winGoal);
|
||||
winScreenUi->SetScreen(*CreateWinScreen(scene, *hud));
|
||||
}
|
||||
|
||||
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);
|
||||
CreateHelmetSpotlight(scene);
|
||||
CreateTree(scene, gfxDevice, resources);
|
||||
// CreateCapybara(scene, gfxDevice, resources);
|
||||
CreateTriggerZone(scene);
|
||||
const GameObject* door = CreateDoorway(scene, gfxDevice, resources);
|
||||
CreateButton(scene, gfxDevice, resources, *door);
|
||||
CreateHouse(scene, gfxDevice, resources);
|
||||
CreateMovingPlatform(scene, gfxDevice, resources);
|
||||
const WinGoal* winGoal = CreateWinCourse(scene, gfxDevice, resources);
|
||||
CreatePlayer(scene, objects);
|
||||
CreateHud(scene, *objects.player, *winGoal->GetGameObject());
|
||||
LoadSpawnBallResources(gfxDevice, resources, objects);
|
||||
|
||||
return objects;
|
||||
}
|
||||
Reference in New Issue
Block a user