diff --git a/CMakeLists.txt b/CMakeLists.txt index db92c13..70e1eb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -302,6 +302,7 @@ set(WORKFLOW_SOURCES set(FRAME_WORKFLOW_SOURCES src/services/impl/workflow_frame_begin_step.cpp + src/services/impl/workflow_frame_camera_step.cpp src/services/impl/workflow_frame_bullet_physics_step.cpp src/services/impl/workflow_frame_physics_step.cpp src/services/impl/workflow_frame_scene_step.cpp diff --git a/packages/seed/workflows/demo_gameplay.json b/packages/seed/workflows/demo_gameplay.json index e8afd9c..af29463 100644 --- a/packages/seed/workflows/demo_gameplay.json +++ b/packages/seed/workflows/demo_gameplay.json @@ -16,6 +16,9 @@ "position": [260, 0], "inputs": { "delta": "frame.delta" + }, + "outputs": { + "view_state": "frame.view_state" } }, { @@ -39,7 +42,8 @@ "plugin": "frame.render", "position": [1040, 0], "inputs": { - "elapsed": "frame.elapsed" + "elapsed": "frame.elapsed", + "view_state": "frame.view_state" } }, { diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index c943892..90d64bb 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -307,6 +307,7 @@ void ServiceBasedApp::RegisterServices() { registry_.RegisterService( registry_.GetService(), + registry_.GetService(), registry_.GetService(), registry_.GetService(), registry_.GetService(), diff --git a/src/services/impl/frame_workflow_service.cpp b/src/services/impl/frame_workflow_service.cpp index 0fd4ad2..5e84113 100644 --- a/src/services/impl/frame_workflow_service.cpp +++ b/src/services/impl/frame_workflow_service.cpp @@ -9,6 +9,7 @@ namespace sdl3cpp::services::impl { FrameWorkflowService::FrameWorkflowService(std::shared_ptr logger, + std::shared_ptr configService, std::shared_ptr audioService, std::shared_ptr inputService, std::shared_ptr physicsService, @@ -27,6 +28,7 @@ FrameWorkflowService::FrameWorkflowService(std::shared_ptr logger, workflow_ = parser.ParseFile(path); FrameWorkflowStepRegistrar registrar(logger_, + std::move(configService), std::move(audioService), std::move(inputService), std::move(physicsService), diff --git a/src/services/impl/frame_workflow_service.hpp b/src/services/impl/frame_workflow_service.hpp index 364d0f7..3b39b36 100644 --- a/src/services/impl/frame_workflow_service.hpp +++ b/src/services/impl/frame_workflow_service.hpp @@ -3,6 +3,7 @@ #include "../interfaces/i_frame_workflow_service.hpp" #include "../interfaces/i_logger.hpp" #include "../interfaces/i_audio_service.hpp" +#include "../interfaces/i_config_service.hpp" #include "../interfaces/i_input_service.hpp" #include "../interfaces/i_physics_service.hpp" #include "../interfaces/i_render_coordinator_service.hpp" @@ -21,6 +22,7 @@ namespace sdl3cpp::services::impl { class FrameWorkflowService final : public IFrameWorkflowService { public: FrameWorkflowService(std::shared_ptr logger, + std::shared_ptr configService, std::shared_ptr audioService, std::shared_ptr inputService, std::shared_ptr physicsService, diff --git a/src/services/impl/frame_workflow_step_registrar.cpp b/src/services/impl/frame_workflow_step_registrar.cpp index 448105a..2ec7ab9 100644 --- a/src/services/impl/frame_workflow_step_registrar.cpp +++ b/src/services/impl/frame_workflow_step_registrar.cpp @@ -2,6 +2,7 @@ #include "workflow_frame_audio_step.hpp" #include "workflow_frame_begin_step.hpp" +#include "workflow_frame_camera_step.hpp" #include "workflow_frame_bullet_physics_step.hpp" #include "workflow_frame_gui_step.hpp" #include "workflow_frame_physics_step.hpp" @@ -16,6 +17,7 @@ namespace sdl3cpp::services::impl { FrameWorkflowStepRegistrar::FrameWorkflowStepRegistrar(std::shared_ptr logger, + std::shared_ptr configService, std::shared_ptr audioService, std::shared_ptr inputService, std::shared_ptr physicsService, @@ -23,6 +25,7 @@ FrameWorkflowStepRegistrar::FrameWorkflowStepRegistrar(std::shared_ptr std::shared_ptr renderService, std::shared_ptr validationTourService) : logger_(std::move(logger)), + configService_(std::move(configService)), audioService_(std::move(audioService)), inputService_(std::move(inputService)), physicsService_(std::move(physicsService)), @@ -50,6 +53,9 @@ void FrameWorkflowStepRegistrar::RegisterUsedSteps( if (plugins.contains("frame.bullet_physics")) { registry->RegisterStep(std::make_shared(physicsService_, logger_)); } + if (plugins.contains("frame.camera")) { + registry->RegisterStep(std::make_shared(configService_, logger_)); + } if (plugins.contains("frame.scene")) { registry->RegisterStep(std::make_shared(sceneService_, logger_)); } diff --git a/src/services/impl/frame_workflow_step_registrar.hpp b/src/services/impl/frame_workflow_step_registrar.hpp index 75752cd..97ac3b8 100644 --- a/src/services/impl/frame_workflow_step_registrar.hpp +++ b/src/services/impl/frame_workflow_step_registrar.hpp @@ -8,6 +8,7 @@ namespace sdl3cpp::services { class IAudioService; +class IConfigService; class IInputService; class IPhysicsService; class IRenderCoordinatorService; @@ -20,6 +21,7 @@ namespace sdl3cpp::services::impl { class FrameWorkflowStepRegistrar { public: FrameWorkflowStepRegistrar(std::shared_ptr logger, + std::shared_ptr configService, std::shared_ptr audioService, std::shared_ptr inputService, std::shared_ptr physicsService, @@ -32,6 +34,7 @@ public: private: std::shared_ptr logger_; + std::shared_ptr configService_; std::shared_ptr audioService_; std::shared_ptr inputService_; std::shared_ptr physicsService_; diff --git a/src/services/impl/render_coordinator_service.cpp b/src/services/impl/render_coordinator_service.cpp index f572ed1..0ee330b 100644 --- a/src/services/impl/render_coordinator_service.cpp +++ b/src/services/impl/render_coordinator_service.cpp @@ -132,6 +132,14 @@ void RenderCoordinatorService::ConfigureRenderGraphPasses() { } void RenderCoordinatorService::RenderFrame(float time) { + RenderFrameInternal(time, nullptr); +} + +void RenderCoordinatorService::RenderFrameWithViewState(float time, const ViewState& viewState) { + RenderFrameInternal(time, &viewState); +} + +void RenderCoordinatorService::RenderFrameInternal(float time, const ViewState* overrideView) { if (logger_) { logger_->Trace("RenderCoordinatorService", "RenderFrame", "time=" + std::to_string(time), "Entering"); } @@ -244,7 +252,7 @@ void RenderCoordinatorService::RenderFrame(float time) { validationPlan = validationTourService_->BeginFrame(aspect); } - ViewState viewState = sceneScriptService_->GetViewState(aspect); + ViewState viewState = overrideView ? *overrideView : sceneScriptService_->GetViewState(aspect); if (validationPlan.active && validationPlan.overrideView) { viewState = validationPlan.viewState; } diff --git a/src/services/impl/render_coordinator_service.hpp b/src/services/impl/render_coordinator_service.hpp index abfb762..cbf4c3c 100644 --- a/src/services/impl/render_coordinator_service.hpp +++ b/src/services/impl/render_coordinator_service.hpp @@ -30,9 +30,9 @@ public: ~RenderCoordinatorService() override = default; void RenderFrame(float time) override; + void RenderFrameWithViewState(float time, const ViewState& viewState) override; private: - void ConfigureRenderGraphPasses(); std::shared_ptr logger_; std::shared_ptr configService_; @@ -49,6 +49,9 @@ private: bool shadersLoaded_ = false; bool geometryUploaded_ = false; bool configFirstLogged_ = false; + + void ConfigureRenderGraphPasses(); + void RenderFrameInternal(float time, const ViewState* overrideView); }; } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/workflow_frame_camera_step.cpp b/src/services/impl/workflow_frame_camera_step.cpp new file mode 100644 index 0000000..767afc1 --- /dev/null +++ b/src/services/impl/workflow_frame_camera_step.cpp @@ -0,0 +1,160 @@ +#include "workflow_frame_camera_step.hpp" +#include "workflow_step_io_resolver.hpp" + +#include "../interfaces/graphics_types.hpp" + +#include + +#include +#include +#include + +#include +#include +#include + +namespace sdl3cpp::services::impl { +namespace { + +struct CameraConfig { + std::array position{0.0f, 0.0f, 5.0f}; + std::array lookAt{0.0f, 0.0f, 0.0f}; + std::array up{0.0f, 1.0f, 0.0f}; + float fov = 0.78f; + float nearPlane = 0.1f; + float farPlane = 1000.0f; +}; + +std::array ToArray(const glm::mat4& matrix) { + std::array result{}; + std::memcpy(result.data(), glm::value_ptr(matrix), sizeof(float) * result.size()); + return result; +} + +bool ReadVec3(const rapidjson::Value& object, const char* name, std::array& out) { + if (!object.HasMember(name)) { + return false; + } + const auto& value = object[name]; + if (!value.IsArray() || value.Size() != 3) { + throw std::runtime_error(std::string("frame.camera: '") + name + "' must be a 3-element array"); + } + for (rapidjson::SizeType i = 0; i < 3; ++i) { + if (!value[i].IsNumber()) { + throw std::runtime_error(std::string("frame.camera: '") + name + "' must contain numbers"); + } + out[i] = static_cast(value[i].GetDouble()); + } + return true; +} + +bool ReadNumber(const rapidjson::Value& object, const char* name, float& out) { + if (!object.HasMember(name)) { + return false; + } + const auto& value = object[name]; + if (!value.IsNumber()) { + throw std::runtime_error(std::string("frame.camera: '") + name + "' must be a number"); + } + out = static_cast(value.GetDouble()); + return true; +} + +CameraConfig ReadCameraConfig(const std::string& json) { + rapidjson::Document document; + document.Parse(json.c_str()); + if (document.HasParseError()) { + throw std::runtime_error("frame.camera: failed to parse config JSON"); + } + CameraConfig config{}; + if (!document.HasMember("scene") || !document["scene"].IsObject()) { + return config; + } + const auto& scene = document["scene"]; + if (!scene.HasMember("camera") || !scene["camera"].IsObject()) { + return config; + } + const auto& camera = scene["camera"]; + ReadVec3(camera, "position", config.position); + bool hasLookAt = ReadVec3(camera, "look_at", config.lookAt); + if (!hasLookAt && camera.HasMember("lookAt")) { + ReadVec3(camera, "lookAt", config.lookAt); + hasLookAt = true; + } + if (!hasLookAt) { + config.lookAt = {config.position[0], config.position[1], config.position[2] - 1.0f}; + } + ReadVec3(camera, "up", config.up); + if (ReadNumber(camera, "fov_degrees", config.fov)) { + config.fov = glm::radians(config.fov); + } else if (ReadNumber(camera, "fov", config.fov) && config.fov > 3.2f) { + config.fov = glm::radians(config.fov); + } + ReadNumber(camera, "near", config.nearPlane); + ReadNumber(camera, "far", config.farPlane); + return config; +} + +ViewState BuildViewState(const CameraConfig& config, float aspect) { + glm::vec3 position(config.position[0], config.position[1], config.position[2]); + glm::vec3 lookAt(config.lookAt[0], config.lookAt[1], config.lookAt[2]); + glm::vec3 up(config.up[0], config.up[1], config.up[2]); + glm::mat4 view = glm::lookAt(position, lookAt, up); + float safeAspect = aspect <= 0.0f ? 1.0f : aspect; + glm::mat4 proj = glm::perspective(config.fov, safeAspect, config.nearPlane, config.farPlane); + + ViewState state{}; + state.view = ToArray(view); + state.proj = ToArray(proj); + state.viewProj = ToArray(proj * view); + state.cameraPosition = config.position; + return state; +} + +} // namespace + +WorkflowFrameCameraStep::WorkflowFrameCameraStep(std::shared_ptr configService, + std::shared_ptr logger) + : configService_(std::move(configService)), + logger_(std::move(logger)) {} + +std::string WorkflowFrameCameraStep::GetPluginId() const { + return "frame.camera"; +} + +void WorkflowFrameCameraStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + if (!configService_) { + throw std::runtime_error("frame.camera requires an IConfigService"); + } + const std::string& configJson = configService_->GetConfigJson(); + if (configJson.empty()) { + throw std::runtime_error("frame.camera requires config JSON to be available"); + } + WorkflowStepIoResolver resolver; + const std::string deltaKey = resolver.GetRequiredInputKey(step, "delta"); + const std::string outputKey = resolver.GetRequiredOutputKey(step, "view_state"); + const auto* delta = context.TryGet(deltaKey); + if (!delta) { + throw std::runtime_error("frame.camera missing delta input"); + } + + CameraConfig cameraConfig = ReadCameraConfig(configJson); + float aspect = 1.0f; + uint32_t width = configService_->GetWindowWidth(); + uint32_t height = configService_->GetWindowHeight(); + if (width > 0 && height > 0) { + aspect = static_cast(width) / static_cast(height); + } + ViewState viewState = BuildViewState(cameraConfig, aspect); + context.Set(outputKey, viewState); + + if (logger_) { + logger_->Trace("WorkflowFrameCameraStep", "Execute", + "delta=" + std::to_string(*delta) + + ", output=" + outputKey + + ", aspect=" + std::to_string(aspect), + "Camera view state updated"); + } +} + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/workflow_frame_camera_step.hpp b/src/services/impl/workflow_frame_camera_step.hpp new file mode 100644 index 0000000..f14debf --- /dev/null +++ b/src/services/impl/workflow_frame_camera_step.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "../interfaces/i_config_service.hpp" +#include "../interfaces/i_logger.hpp" +#include "../interfaces/i_workflow_step.hpp" + +#include +#include + +namespace sdl3cpp::services::impl { + +class WorkflowFrameCameraStep final : public IWorkflowStep { +public: + WorkflowFrameCameraStep(std::shared_ptr configService, + std::shared_ptr logger); + + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; + +private: + std::shared_ptr configService_; + std::shared_ptr logger_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/workflow_frame_render_step.cpp b/src/services/impl/workflow_frame_render_step.cpp index d5f6af3..69ace8b 100644 --- a/src/services/impl/workflow_frame_render_step.cpp +++ b/src/services/impl/workflow_frame_render_step.cpp @@ -24,7 +24,19 @@ void WorkflowFrameRenderStep::Execute(const WorkflowStepDefinition& step, Workfl if (!elapsed) { throw std::runtime_error("frame.render missing elapsed input"); } - renderService_->RenderFrame(static_cast(*elapsed)); + const ViewState* viewState = nullptr; + auto it = step.inputs.find("view_state"); + if (it != step.inputs.end()) { + viewState = context.TryGet(it->second); + if (!viewState) { + throw std::runtime_error("frame.render missing view_state input"); + } + } + if (viewState) { + renderService_->RenderFrameWithViewState(static_cast(*elapsed), *viewState); + } else { + renderService_->RenderFrame(static_cast(*elapsed)); + } if (logger_) { logger_->Trace("WorkflowFrameRenderStep", "Execute", "elapsed=" + std::to_string(*elapsed), diff --git a/src/services/interfaces/i_render_coordinator_service.hpp b/src/services/interfaces/i_render_coordinator_service.hpp index f8f8080..0d8f2ff 100644 --- a/src/services/interfaces/i_render_coordinator_service.hpp +++ b/src/services/interfaces/i_render_coordinator_service.hpp @@ -1,5 +1,7 @@ #pragma once +#include "graphics_types.hpp" + namespace sdl3cpp::services { class IRenderCoordinatorService { @@ -7,6 +9,7 @@ public: virtual ~IRenderCoordinatorService() = default; virtual void RenderFrame(float time) = 0; + virtual void RenderFrameWithViewState(float time, const ViewState& viewState) = 0; }; } // namespace sdl3cpp::services