diff --git a/CMakeLists.txt b/CMakeLists.txt index b4280af..c132eaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,11 +125,22 @@ target_link_libraries(render_renderer PUBLIC render::core render::platform rende render_apply_project_options(render_renderer) render_apply_warnings(render_renderer) + +add_library(render_scene STATIC + engine/scene/scene.hpp + engine/scene/scene.cpp +) +add_library(render::scene ALIAS render_scene) +target_include_directories(render_scene PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(render_scene PUBLIC render::core render::renderer) +render_apply_project_options(render_scene) +render_apply_warnings(render_scene) + add_library(render_engine STATIC engine/engine.cpp ) add_library(render::engine ALIAS render_engine) -target_link_libraries(render_engine PUBLIC render::core render::filesystem render::platform render::renderer render::serialization) +target_link_libraries(render_engine PUBLIC render::core render::filesystem render::platform render::renderer render::serialization render::scene) render_apply_project_options(render_engine) render_apply_warnings(render_engine) @@ -193,6 +204,11 @@ if(RENDER_BUILD_TESTS) render_apply_project_options(render_shader_pipeline_tests) render_apply_warnings(render_shader_pipeline_tests) + add_executable(render_scene_tests tests/scene/scene_tests.cpp) + target_link_libraries(render_scene_tests PRIVATE render::scene) + render_apply_project_options(render_scene_tests) + render_apply_warnings(render_scene_tests) + add_test(NAME unit.platform.types COMMAND render_platform_types_tests) set_tests_properties(unit.platform.types PROPERTIES LABELS "unit;platform") add_test(NAME unit.core.runtime COMMAND render_core_runtime_tests) @@ -205,12 +221,14 @@ if(RENDER_BUILD_TESTS) set_tests_properties(unit.renderer.lifecycle PROPERTIES LABELS "unit;renderer") add_test(NAME unit.renderer.shader_pipeline COMMAND render_shader_pipeline_tests) set_tests_properties(unit.renderer.shader_pipeline PROPERTIES LABELS "unit;renderer;shader") + add_test(NAME unit.scene.runtime COMMAND render_scene_tests) + set_tests_properties(unit.scene.runtime PROPERTIES LABELS "unit;scene") add_test(NAME headless.smoke.startup COMMAND render_headless_validation) set_tests_properties(headless.smoke.startup PROPERTIES LABELS "headless;smoke") add_custom_target(render_test_unit COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure --label-regex unit - DEPENDS render_platform_types_tests render_core_runtime_tests render_serialization_tests render_filesystem_tests render_renderer_lifecycle_tests render_shader_pipeline_tests + DEPENDS render_platform_types_tests render_core_runtime_tests render_serialization_tests render_filesystem_tests render_renderer_lifecycle_tests render_shader_pipeline_tests render_scene_tests WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" COMMENT "Running render unit tests" ) diff --git a/docs/architecture.md b/docs/architecture.md index ceb36c1..bbe1422 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,4 +1,4 @@ -# Architecture Snapshot (Statement 5) +# Architecture Snapshot (Statements 5-11) ## Runtime layering now implemented @@ -34,3 +34,9 @@ 5. **Core runtime layer (`engine/core`)** - Foundational math, transforms, camera helpers, colors, UUID/hash/random utilities. - Shared runtime services for memory, threading, logging, assertions, and profiling hooks. + + +6. **Scene runtime layer (`engine/scene`)** + - Engine-owned scene graph with stable node handles, transform hierarchy, visibility, and debug names. + - Optional camera/light/renderable attachments with traversal and renderer extraction helpers. + - Explicit update step for dirty world transform propagation (`Scene::update_world_transforms`). diff --git a/docs/rendering.md b/docs/rendering.md index cb5a09d..2415bda 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -82,3 +82,15 @@ Deferred to later statements: - compute shader runtime integration (manifest structure already reserves this) - centralized material system on top of `ShaderProgramId` - full editor/content pipeline tooling around variants and dependency graphs + + +## Scene integration (Statement 11) + +`render_shell` now drives rendering through `engine/scene`: + +- scene nodes carry camera/light/renderable attachments +- frame flow calls `Scene::update_world_transforms()` before extraction +- view matrices come from `Scene::build_camera_view(...)` +- draw submissions are emitted from `Scene::collect_visible_renderables(...)` + +This establishes a stable engine-owned scene/runtime boundary above the renderer API. diff --git a/docs/scene.md b/docs/scene.md new file mode 100644 index 0000000..c0e9993 --- /dev/null +++ b/docs/scene.md @@ -0,0 +1,106 @@ +# Scene System (Statement 11) + +## Module layout + +- `engine/scene/scene.hpp`: public scene API and component data. +- `engine/scene/scene.cpp`: handle validation, hierarchy, dirty propagation, traversal, extraction. +- `engine/scene/README.md`: focused module notes. + +The scene module is engine-owned and only references engine-owned core/render interfaces. + +## Identity model + +Each node uses `SceneNodeId { index, generation }`: + +- `index` addresses storage slot +- `generation` invalidates stale handles after destroy/reuse + +Destroyed nodes increment generation and return slot to a free-list. This prevents stale handles from silently targeting unrelated nodes. + +## Transform + hierarchy rules + +Each node stores: + +- local transform (`core::Transform`) +- cached world transform (`core::Transform`) +- parent and children links + +`Scene::update_world_transforms()` performs explicit top-down propagation from roots. + +- Dirty state is pushed to descendants on local transform or hierarchy changes. +- Recompute is skipped for nodes that are not dirty and do not have dirty ancestry. + +Reparenting policies: + +- keep local: retain local transform values +- keep world: solve new local transform from current world and target parent world + +Cycles are rejected with ancestor walk checks. + +## Camera/light/renderable representation + +### Cameras + +`CameraComponent` supports: + +- perspective + orthographic params (via `core::camera` types) +- enabled flag +- layer mask for filtering compatibility + +`build_camera_view(aspect_ratio)` resolves active/fallback camera and returns derived view/projection matrices. + +### Lights + +`LightComponent` supports: + +- directional / point types +- color, intensity, range +- enabled flag + +Light direction/position come from node world transform. + +### Renderables + +`RenderableComponent` supports: + +- mesh handle references +- program handle references +- draw state +- enabled flag + layer mask + +Scene APIs keep renderer backend details hidden (no raw bgfx exposure). + +## Visibility + debug names + +Nodes expose: + +- `NodeVisibility { enabled, layer_mask }` +- debug names as human-readable labels + +Duplicate names are allowed. `find_nodes_by_name` returns all matches for tooling use. + +## Renderer integration at this stage + +`render_shell` now builds and renders through the scene: + +- creates root/camera/light/renderable nodes +- parents nodes into hierarchy +- updates scene transforms each frame +- derives camera view/projection from scene +- collects visible renderables and submits to renderer + +This validates the scene as the canonical runtime spatial layer. + +## Current scope vs deferred + +Implemented now: + +- core node runtime, hierarchy, transforms, cameras/lights/renderables +- traversal/query/extraction helpers +- unit tests for handle, hierarchy, transform, components, visibility, naming + +Deferred: + +- binary/text scene serialization schema and IO flow +- culling structures and batching +- richer editor/runtime inspection APIs diff --git a/engine/scene/README.md b/engine/scene/README.md new file mode 100644 index 0000000..3cf02c8 --- /dev/null +++ b/engine/scene/README.md @@ -0,0 +1,46 @@ +# Scene Runtime (Statement 11) + +`engine/scene` provides the engine-owned spatial scene foundation. + +## Current capabilities + +- Stable scene node handles with generation checks (`SceneNodeId`). +- Parent-child hierarchy with cycle prevention and explicit reparent policy. +- Local + world transforms using `core::Transform` and explicit `update_world_transforms()`. +- Optional node attachments for camera, light, and renderable data. +- Node visibility + layer masks and renderable-specific layer masks. +- Debug names and duplicate-name lookup. +- Scene traversal and renderer-facing extraction helpers (`build_camera_view`, `collect_visible_renderables`, `collect_visible_lights`). + +## Update model + +The scene does not mutate world transforms implicitly for every operation. Call `Scene::update_world_transforms()` after structural/transform edits and before extraction/submission. + +Dirty propagation rules: + +- writing local transform marks node subtree dirty +- reparent/unparent marks affected subtree dirty +- update pass recomputes world transforms top-down from roots + +## Reparent policy + +`set_parent` and `clear_parent` take `ReparentPolicy`: + +- `KeepLocalTransform`: preserve local transform fields, world changes relative to new parent +- `KeepWorldTransform`: preserve world transform and recompute local relative to new parent + +## Visibility rules + +A node contributes renderables/lights only when: + +- node visibility `enabled == true` +- component `enabled == true` +- node and component layer masks overlap caller-provided layer mask + +## Deferred for later statements + +- scene serialization format + load/save pipelines +- culling acceleration structures +- advanced camera controllers and camera blending +- shadow map authoring and broader light model +- material graph / resource binding systems diff --git a/engine/scene/scene.cpp b/engine/scene/scene.cpp new file mode 100644 index 0000000..9c0b551 --- /dev/null +++ b/engine/scene/scene.cpp @@ -0,0 +1,607 @@ +#include "engine/scene/scene.hpp" + +#include "engine/core/assert.hpp" +#include "engine/core/log.hpp" + +#include +#include +#include + +namespace render::scene { + +namespace { + +core::Vec3 multiply_components(const core::Vec3& a, const core::Vec3& b) { + return {a.x * b.x, a.y * b.y, a.z * b.z}; +} + +core::Quaternion conjugate(const core::Quaternion& q) { + return {-q.x, -q.y, -q.z, q.w}; +} + +core::Vec3 rotate_vector(const core::Quaternion& q, const core::Vec3& v) { + const core::Quaternion p{v.x, v.y, v.z, 0.0F}; + const core::Quaternion rotated = q * p * conjugate(q); + return {rotated.x, rotated.y, rotated.z}; +} + +core::Vec3 safe_divide_components(const core::Vec3& numer, const core::Vec3& denom) { + const auto div = [](const float n, const float d) { + return std::fabs(d) > core::kEpsilon ? (n / d) : 0.0F; + }; + return {div(numer.x, denom.x), div(numer.y, denom.y), div(numer.z, denom.z)}; +} + +} // namespace + +SceneNodeId Scene::create_node(std::string debug_name) { + std::uint32_t index = 0; + if (!free_indices_.empty()) { + index = free_indices_.back(); + free_indices_.pop_back(); + } else { + index = static_cast(nodes_.size()); + nodes_.push_back({}); + } + + NodeRecord& record = nodes_[index]; + record.alive = true; + record.parent.reset(); + record.children.clear(); + record.local = core::Transform::identity(); + record.world = core::Transform::identity(); + record.world_dirty = true; + record.visibility = {}; + record.debug_name = std::move(debug_name); + record.camera.reset(); + record.light.reset(); + record.renderable.reset(); + + const SceneNodeId node{index, record.generation}; + RENDER_LOG_INFO("scene", "create node idx=", node.index, " name='", record.debug_name, "'"); + return node; +} + +bool Scene::destroy_node(const SceneNodeId node) { + if (!is_alive(node)) { + return false; + } + + if (active_camera_.has_value() && active_camera_.value() == node) { + active_camera_.reset(); + } + + destroy_subtree(node); + return true; +} + +bool Scene::is_alive(const SceneNodeId node) const { + const NodeRecord* record = lookup(node); + return record != nullptr; +} + +bool Scene::set_parent(const SceneNodeId node, const SceneNodeId parent, const ReparentPolicy policy) { + if (!is_alive(node) || !is_alive(parent) || node == parent || would_create_cycle(node, parent)) { + RENDER_LOG_WARN("scene", "rejected set_parent node=", node.index, " parent=", parent.index); + return false; + } + + NodeRecord* node_record = lookup(node); + RENDER_ASSERT(node_record != nullptr, "node should be valid"); + + const core::Transform prior_world = node_record->world; + + detach_from_parent(node); + + NodeRecord* parent_record = lookup(parent); + RENDER_ASSERT(parent_record != nullptr, "parent should be valid"); + node_record->parent = parent; + parent_record->children.push_back(node); + + if (policy == ReparentPolicy::KeepWorldTransform) { + node_record->local = relative_transform(parent_record->world, prior_world); + } + + mark_world_dirty_recursive(node); + return true; +} + +bool Scene::clear_parent(const SceneNodeId node, const ReparentPolicy policy) { + if (!is_alive(node)) { + return false; + } + + NodeRecord* node_record = lookup(node); + RENDER_ASSERT(node_record != nullptr, "node should be valid"); + if (!node_record->parent.has_value()) { + return true; + } + + const core::Transform prior_world = node_record->world; + detach_from_parent(node); + + if (policy == ReparentPolicy::KeepWorldTransform) { + node_record->local = prior_world; + } + + mark_world_dirty_recursive(node); + return true; +} + +std::optional Scene::parent(const SceneNodeId node) const { + const NodeRecord* record = lookup(node); + if (record == nullptr) { + return std::nullopt; + } + return record->parent; +} + +const std::vector* Scene::children(const SceneNodeId node) const { + const NodeRecord* record = lookup(node); + if (record == nullptr) { + return nullptr; + } + return &record->children; +} + +std::vector Scene::root_nodes() const { + std::vector roots; + for (std::uint32_t i = 0; i < nodes_.size(); ++i) { + const NodeRecord& record = nodes_[i]; + if (record.alive && !record.parent.has_value()) { + roots.push_back({i, record.generation}); + } + } + return roots; +} + +bool Scene::set_local_transform(const SceneNodeId node, const core::Transform& local) { + NodeRecord* record = lookup(node); + if (record == nullptr) { + return false; + } + record->local = local; + mark_world_dirty_recursive(node); + return true; +} + +const core::Transform* Scene::local_transform(const SceneNodeId node) const { + const NodeRecord* record = lookup(node); + return record == nullptr ? nullptr : &record->local; +} + +const core::Transform* Scene::world_transform(const SceneNodeId node) const { + const NodeRecord* record = lookup(node); + return record == nullptr ? nullptr : &record->world; +} + +bool Scene::set_visibility(const SceneNodeId node, const NodeVisibility visibility) { + NodeRecord* record = lookup(node); + if (record == nullptr) { + return false; + } + record->visibility = visibility; + return true; +} + +std::optional Scene::visibility(const SceneNodeId node) const { + const NodeRecord* record = lookup(node); + if (record == nullptr) { + return std::nullopt; + } + return record->visibility; +} + +bool Scene::set_debug_name(const SceneNodeId node, std::string name) { + NodeRecord* record = lookup(node); + if (record == nullptr) { + return false; + } + record->debug_name = std::move(name); + return true; +} + +const std::string* Scene::debug_name(const SceneNodeId node) const { + const NodeRecord* record = lookup(node); + return record == nullptr ? nullptr : &record->debug_name; +} + +std::vector Scene::find_nodes_by_name(const std::string_view name) const { + std::vector matches; + for (std::uint32_t i = 0; i < nodes_.size(); ++i) { + const NodeRecord& record = nodes_[i]; + if (record.alive && record.debug_name == name) { + matches.push_back({i, record.generation}); + } + } + return matches; +} + +bool Scene::set_camera(const SceneNodeId node, const CameraComponent& camera_component) { + NodeRecord* record = lookup(node); + if (record == nullptr) { + return false; + } + record->camera = camera_component; + return true; +} + +bool Scene::clear_camera(const SceneNodeId node) { + NodeRecord* record = lookup(node); + if (record == nullptr) { + return false; + } + record->camera.reset(); + if (active_camera_.has_value() && active_camera_.value() == node) { + active_camera_.reset(); + } + return true; +} + +const CameraComponent* Scene::camera(const SceneNodeId node) const { + const NodeRecord* record = lookup(node); + if (record == nullptr || !record->camera.has_value()) { + return nullptr; + } + return &record->camera.value(); +} + +bool Scene::set_light(const SceneNodeId node, const LightComponent& light_component) { + NodeRecord* record = lookup(node); + if (record == nullptr) { + return false; + } + record->light = light_component; + return true; +} + +bool Scene::clear_light(const SceneNodeId node) { + NodeRecord* record = lookup(node); + if (record == nullptr) { + return false; + } + record->light.reset(); + return true; +} + +const LightComponent* Scene::light(const SceneNodeId node) const { + const NodeRecord* record = lookup(node); + if (record == nullptr || !record->light.has_value()) { + return nullptr; + } + return &record->light.value(); +} + +bool Scene::set_renderable(const SceneNodeId node, const RenderableComponent& renderable_component) { + NodeRecord* record = lookup(node); + if (record == nullptr) { + return false; + } + record->renderable = renderable_component; + return true; +} + +bool Scene::clear_renderable(const SceneNodeId node) { + NodeRecord* record = lookup(node); + if (record == nullptr) { + return false; + } + record->renderable.reset(); + return true; +} + +const RenderableComponent* Scene::renderable(const SceneNodeId node) const { + const NodeRecord* record = lookup(node); + if (record == nullptr || !record->renderable.has_value()) { + return nullptr; + } + return &record->renderable.value(); +} + +bool Scene::set_active_camera(const SceneNodeId node) { + const NodeRecord* record = lookup(node); + if (record == nullptr || !record->camera.has_value()) { + return false; + } + active_camera_ = node; + RENDER_LOG_INFO("scene", "active camera idx=", node.index); + return true; +} + +void Scene::clear_active_camera() { + active_camera_.reset(); +} + +void Scene::update_world_transforms() { + std::stack stack; + for (const SceneNodeId root : root_nodes()) { + stack.push(root); + while (!stack.empty()) { + const SceneNodeId node = stack.top(); + stack.pop(); + + NodeRecord* record = lookup(node); + if (record == nullptr) { + continue; + } + + bool parent_dirty = false; + if (record->parent.has_value()) { + const NodeRecord* parent_record = lookup(record->parent.value()); + parent_dirty = parent_record != nullptr ? parent_record->world_dirty : false; + } + if (record->world_dirty || parent_dirty) { + if (record->parent.has_value()) { + const NodeRecord* parent_record = lookup(record->parent.value()); + record->world = core::compose(parent_record->world, record->local); + } else { + record->world = record->local; + } + record->world_dirty = false; + } + + for (const SceneNodeId child : record->children) { + stack.push(child); + } + } + } +} + +std::optional Scene::build_camera_view(const float aspect_ratio) const { + std::optional camera_node_id = active_camera_; + if (!camera_node_id.has_value()) { + for (std::uint32_t i = 0; i < nodes_.size(); ++i) { + const NodeRecord& record = nodes_[i]; + if (record.alive && record.camera.has_value() && record.camera->enabled && record.visibility.enabled) { + camera_node_id = SceneNodeId{i, record.generation}; + break; + } + } + } + + if (!camera_node_id.has_value()) { + return std::nullopt; + } + + const NodeRecord* camera_node = lookup(camera_node_id.value()); + if (camera_node == nullptr || !camera_node->camera.has_value()) { + return std::nullopt; + } + + const CameraComponent& camera_component = camera_node->camera.value(); + if (!camera_component.enabled || !camera_node->visibility.enabled) { + return std::nullopt; + } + + core::Mat4 projection = core::Mat4::identity(); + if (camera_component.projection == CameraProjection::Perspective) { + core::PerspectiveCameraParams params = camera_component.perspective; + params.aspect_ratio = aspect_ratio; + projection = core::make_perspective(params); + } else { + projection = core::make_orthographic(camera_component.orthographic); + } + + const core::Vec3 eye = camera_node->world.translation; + const core::Vec3 target = eye + core::forward(camera_node->world); + const core::Vec3 up = core::up(camera_node->world); + + return CameraView{.node = camera_node_id.value(), .view = core::make_look_at(eye, target, up), .projection = projection}; +} + +std::vector Scene::collect_visible_renderables(const std::uint32_t layer_mask) const { + std::vector out; + out.reserve(nodes_.size()); + walk_depth_first([this, &out, layer_mask](const SceneNodeId node) { + const NodeRecord* record = lookup(node); + if (record == nullptr || !record->visibility.enabled || !record->renderable.has_value()) { + return; + } + + const RenderableComponent& renderable_component = record->renderable.value(); + if (!renderable_component.enabled) { + return; + } + + if ((record->visibility.layer_mask & layer_mask) == 0U) { + return; + } + + if ((renderable_component.layer_mask & layer_mask) == 0U) { + return; + } + + out.push_back({.node = node, .renderable = &renderable_component, .world_transform = &record->world}); + }); + return out; +} + +std::vector Scene::collect_visible_lights(const std::uint32_t layer_mask) const { + std::vector out; + out.reserve(nodes_.size()); + walk_depth_first([this, &out, layer_mask](const SceneNodeId node) { + const NodeRecord* record = lookup(node); + if (record == nullptr || !record->visibility.enabled || !record->light.has_value()) { + return; + } + + if ((record->visibility.layer_mask & layer_mask) == 0U) { + return; + } + + const LightComponent& light_component = record->light.value(); + if (!light_component.enabled) { + return; + } + + out.push_back({.node = node, .light = &light_component, .world_transform = &record->world}); + }); + return out; +} + +void Scene::walk_depth_first(const std::function& visitor) const { + std::vector stack = root_nodes(); + while (!stack.empty()) { + const SceneNodeId node = stack.back(); + stack.pop_back(); + + visitor(node); + + const NodeRecord* record = lookup(node); + if (record == nullptr) { + continue; + } + + for (auto it = record->children.rbegin(); it != record->children.rend(); ++it) { + stack.push_back(*it); + } + } +} + +std::size_t Scene::node_count() const noexcept { + std::size_t count = 0; + for (const NodeRecord& record : nodes_) { + if (record.alive) { + ++count; + } + } + return count; +} + +Scene::NodeRecord* Scene::lookup(const SceneNodeId node) { + if (node.index >= nodes_.size()) { + return nullptr; + } + + NodeRecord& record = nodes_[node.index]; + if (!record.alive || record.generation != node.generation) { + return nullptr; + } + + return &record; +} + +const Scene::NodeRecord* Scene::lookup(const SceneNodeId node) const { + if (node.index >= nodes_.size()) { + return nullptr; + } + + const NodeRecord& record = nodes_[node.index]; + if (!record.alive || record.generation != node.generation) { + return nullptr; + } + + return &record; +} + +bool Scene::would_create_cycle(const SceneNodeId node, const SceneNodeId parent) const { + std::optional cursor = parent; + while (cursor.has_value()) { + if (cursor.value() == node) { + return true; + } + + const NodeRecord* record = lookup(cursor.value()); + if (record == nullptr) { + return false; + } + cursor = record->parent; + } + + return false; +} + +void Scene::mark_world_dirty_recursive(const SceneNodeId node) { + NodeRecord* record = lookup(node); + if (record == nullptr) { + return; + } + + std::vector stack; + stack.push_back(node); + while (!stack.empty()) { + const SceneNodeId current = stack.back(); + stack.pop_back(); + + NodeRecord* current_record = lookup(current); + if (current_record == nullptr) { + continue; + } + + current_record->world_dirty = true; + for (const SceneNodeId child : current_record->children) { + stack.push_back(child); + } + } +} + +void Scene::detach_from_parent(const SceneNodeId node) { + NodeRecord* record = lookup(node); + if (record == nullptr || !record->parent.has_value()) { + return; + } + + NodeRecord* parent_record = lookup(record->parent.value()); + if (parent_record != nullptr) { + auto& siblings = parent_record->children; + siblings.erase(std::remove(siblings.begin(), siblings.end(), node), siblings.end()); + } + record->parent.reset(); +} + +void Scene::destroy_subtree(const SceneNodeId node) { + std::vector stack; + std::vector post_order; + stack.push_back(node); + + while (!stack.empty()) { + const SceneNodeId current = stack.back(); + stack.pop_back(); + + const NodeRecord* current_record = lookup(current); + if (current_record == nullptr) { + continue; + } + + post_order.push_back(current); + for (const SceneNodeId child : current_record->children) { + stack.push_back(child); + } + } + + for (auto it = post_order.rbegin(); it != post_order.rend(); ++it) { + NodeRecord* record = lookup(*it); + if (record == nullptr) { + continue; + } + + detach_from_parent(*it); + record = lookup(*it); + if (record == nullptr) { + continue; + } + + record->alive = false; + record->children.clear(); + record->parent.reset(); + record->camera.reset(); + record->light.reset(); + record->renderable.reset(); + ++record->generation; + free_indices_.push_back(it->index); + RENDER_LOG_INFO("scene", "destroy node idx=", it->index); + } +} + +core::Transform Scene::relative_transform(const core::Transform& parent_world, const core::Transform& world) { + core::Transform out{}; + out.rotation = conjugate(parent_world.rotation) * world.rotation; + out.scale = safe_divide_components(world.scale, parent_world.scale); + + const core::Vec3 delta = world.translation - parent_world.translation; + const core::Vec3 unrotated = rotate_vector(conjugate(parent_world.rotation), delta); + out.translation = safe_divide_components(unrotated, parent_world.scale); + return out; +} + +} // namespace render::scene diff --git a/engine/scene/scene.hpp b/engine/scene/scene.hpp new file mode 100644 index 0000000..523904d --- /dev/null +++ b/engine/scene/scene.hpp @@ -0,0 +1,171 @@ +#pragma once + +#include "engine/core/camera.hpp" +#include "engine/core/transform.hpp" +#include "engine/render/renderer_types.hpp" + +#include +#include +#include +#include +#include +#include + +namespace render::scene { + +constexpr std::uint32_t kVisibilityAll = 0xFFFFFFFFu; + +struct SceneNodeId { + std::uint32_t index{0xFFFFFFFFu}; + std::uint32_t generation{0}; + + [[nodiscard]] bool is_valid() const noexcept { return index != 0xFFFFFFFFu; } + friend bool operator==(const SceneNodeId&, const SceneNodeId&) = default; +}; + +enum class ReparentPolicy : std::uint8_t { + KeepLocalTransform = 0, + KeepWorldTransform, +}; + +enum class LightType : std::uint8_t { + Directional = 0, + Point, +}; + +enum class CameraProjection : std::uint8_t { + Perspective = 0, + Orthographic, +}; + +struct NodeVisibility { + bool enabled{true}; + std::uint32_t layer_mask{kVisibilityAll}; +}; + +struct CameraComponent { + bool enabled{true}; + std::uint32_t layer_mask{kVisibilityAll}; + CameraProjection projection{CameraProjection::Perspective}; + core::PerspectiveCameraParams perspective{}; + core::OrthographicCameraParams orthographic{}; +}; + +struct LightComponent { + bool enabled{true}; + LightType type{LightType::Directional}; + core::Vec3 color{1.0F, 1.0F, 1.0F}; + float intensity{1.0F}; + float range{10.0F}; +}; + +struct RenderableComponent { + bool enabled{true}; + std::uint32_t layer_mask{kVisibilityAll}; + rendering::VertexBufferHandle vertex_buffer{}; + rendering::IndexBufferHandle index_buffer{}; + std::uint32_t index_count{0}; + rendering::ProgramHandle program{}; + rendering::DrawState draw_state{}; +}; + +struct CameraView { + SceneNodeId node{}; + core::Mat4 view{}; + core::Mat4 projection{}; +}; + +struct VisibleRenderable { + SceneNodeId node{}; + const RenderableComponent* renderable{nullptr}; + const core::Transform* world_transform{nullptr}; +}; + +struct VisibleLight { + SceneNodeId node{}; + const LightComponent* light{nullptr}; + const core::Transform* world_transform{nullptr}; +}; + +class Scene { +public: + [[nodiscard]] SceneNodeId create_node(std::string debug_name = {}); + bool destroy_node(SceneNodeId node); + + [[nodiscard]] bool is_alive(SceneNodeId node) const; + + bool set_parent(SceneNodeId node, SceneNodeId parent, ReparentPolicy policy = ReparentPolicy::KeepWorldTransform); + bool clear_parent(SceneNodeId node, ReparentPolicy policy = ReparentPolicy::KeepWorldTransform); + + [[nodiscard]] std::optional parent(SceneNodeId node) const; + [[nodiscard]] const std::vector* children(SceneNodeId node) const; + [[nodiscard]] std::vector root_nodes() const; + + bool set_local_transform(SceneNodeId node, const core::Transform& local); + [[nodiscard]] const core::Transform* local_transform(SceneNodeId node) const; + [[nodiscard]] const core::Transform* world_transform(SceneNodeId node) const; + + bool set_visibility(SceneNodeId node, NodeVisibility visibility); + [[nodiscard]] std::optional visibility(SceneNodeId node) const; + + bool set_debug_name(SceneNodeId node, std::string name); + [[nodiscard]] const std::string* debug_name(SceneNodeId node) const; + [[nodiscard]] std::vector find_nodes_by_name(std::string_view name) const; + + bool set_camera(SceneNodeId node, const CameraComponent& camera); + bool clear_camera(SceneNodeId node); + [[nodiscard]] const CameraComponent* camera(SceneNodeId node) const; + + bool set_light(SceneNodeId node, const LightComponent& light); + bool clear_light(SceneNodeId node); + [[nodiscard]] const LightComponent* light(SceneNodeId node) const; + + bool set_renderable(SceneNodeId node, const RenderableComponent& renderable); + bool clear_renderable(SceneNodeId node); + [[nodiscard]] const RenderableComponent* renderable(SceneNodeId node) const; + + bool set_active_camera(SceneNodeId node); + void clear_active_camera(); + [[nodiscard]] std::optional active_camera() const { return active_camera_; } + + void update_world_transforms(); + + [[nodiscard]] std::optional build_camera_view(float aspect_ratio) const; + [[nodiscard]] std::vector collect_visible_renderables(std::uint32_t layer_mask = kVisibilityAll) const; + [[nodiscard]] std::vector collect_visible_lights(std::uint32_t layer_mask = kVisibilityAll) const; + + void walk_depth_first(const std::function& visitor) const; + + [[nodiscard]] std::size_t node_count() const noexcept; + +private: + struct NodeRecord { + bool alive{false}; + std::uint32_t generation{0}; + std::optional parent{}; + std::vector children{}; + core::Transform local{}; + core::Transform world{}; + bool world_dirty{true}; + NodeVisibility visibility{}; + std::string debug_name{}; + std::optional camera{}; + std::optional light{}; + std::optional renderable{}; + }; + + [[nodiscard]] NodeRecord* lookup(SceneNodeId node); + [[nodiscard]] const NodeRecord* lookup(SceneNodeId node) const; + [[nodiscard]] bool would_create_cycle(SceneNodeId node, SceneNodeId parent) const; + void mark_world_dirty_recursive(SceneNodeId node); + void detach_from_parent(SceneNodeId node); + void destroy_subtree(SceneNodeId node); + + static core::Transform relative_transform(const core::Transform& parent_world, const core::Transform& world); + + std::vector nodes_{}; + std::vector free_indices_{}; + std::optional active_camera_{}; +}; + +} // namespace render::scene diff --git a/engine/shell/main.cpp b/engine/shell/main.cpp index c7af4d9..38dadb4 100644 --- a/engine/shell/main.cpp +++ b/engine/shell/main.cpp @@ -3,6 +3,7 @@ #include "engine/platform/platform_runtime.hpp" #include "engine/render/renderer.hpp" #include "engine/render/shader_library.hpp" +#include "engine/scene/scene.hpp" #include #include @@ -48,13 +49,8 @@ render::rendering::RendererBackend parse_renderer_backend_from_args(const int ar return render::rendering::RendererBackend::Auto; } -std::array identity_matrix() { - return { - 1.0F, 0.0F, 0.0F, 0.0F, - 0.0F, 1.0F, 0.0F, 0.0F, - 0.0F, 0.0F, 1.0F, 0.0F, - 0.0F, 0.0F, 0.0F, 1.0F, - }; +std::array to_array(const render::core::Mat4& matrix) { + return matrix.m; } } // namespace @@ -65,7 +61,7 @@ int main(int argc, char** argv) { render::platform::RuntimeConfig platform_config{}; platform_config.app_name = "render-shell"; platform_config.org_name = "render"; - platform_config.window.title = "render :: Statement 9 shell"; + platform_config.window.title = "render :: Statement 11 scene shell"; platform_config.window.width = 1280; platform_config.window.height = 720; platform_config.window.resizable = true; @@ -136,7 +132,36 @@ int main(int argc, char** argv) { }; render::rendering::ProgramHandle program = shader_library.load_program(shader_id); - const auto identity = identity_matrix(); + render::scene::Scene scene; + const render::scene::SceneNodeId root = scene.create_node("root"); + const render::scene::SceneNodeId camera_node = scene.create_node("main_camera"); + const render::scene::SceneNodeId light_node = scene.create_node("sun"); + const render::scene::SceneNodeId mesh_node = scene.create_node("triangle_mesh"); + + render::core::Transform camera_local{}; + camera_local.translation = {0.0F, 0.0F, 3.0F}; + scene.set_local_transform(camera_node, camera_local); + + render::scene::CameraComponent camera{}; + camera.projection = render::scene::CameraProjection::Perspective; + scene.set_camera(camera_node, camera); + scene.set_active_camera(camera_node); + + render::scene::LightComponent light{}; + light.type = render::scene::LightType::Directional; + light.intensity = 4.0F; + scene.set_light(light_node, light); + + render::scene::RenderableComponent renderable{}; + renderable.vertex_buffer = vertex_buffer; + renderable.index_buffer = index_buffer; + renderable.index_count = static_cast(indices.size()); + renderable.program = program; + scene.set_renderable(mesh_node, renderable); + + scene.set_parent(camera_node, root, render::scene::ReparentPolicy::KeepLocalTransform); + scene.set_parent(light_node, root, render::scene::ReparentPolicy::KeepLocalTransform); + scene.set_parent(mesh_node, root, render::scene::ReparentPolicy::KeepLocalTransform); while (!runtime.should_quit()) { runtime.begin_frame(); @@ -164,19 +189,42 @@ int main(int argc, char** argv) { constexpr render::rendering::ViewId kMainView{0}; renderer.set_view(kMainView, view); - renderer.set_view_transform(kMainView, std::span{identity}, std::span{identity}); shader_library.reload_if_stale(shader_id, program); if (program.idx != render::rendering::kInvalidHandle) { + renderable.program = program; + scene.set_renderable(mesh_node, renderable); + } + + scene.update_world_transforms(); + + const float aspect_ratio = static_cast(window.width) / static_cast(window.height); + const std::optional camera_view = scene.build_camera_view(aspect_ratio); + if (camera_view.has_value()) { + const std::array view_matrix = to_array(camera_view->view); + const std::array projection_matrix = to_array(camera_view->projection); + renderer.set_view_transform(kMainView, + std::span{view_matrix}, + std::span{projection_matrix}); + } + + const std::vector draw_list = scene.collect_visible_renderables(); + for (const render::scene::VisibleRenderable& visible : draw_list) { + if (visible.renderable == nullptr || visible.world_transform == nullptr) { + continue; + } + + const std::array world_matrix = to_array(visible.world_transform->to_matrix()); render::rendering::MeshSubmission submission{}; submission.mesh = render::rendering::MeshHandle{ - .vertex_buffer = vertex_buffer, - .index_buffer = index_buffer, - .index_count = static_cast(indices.size()), + .vertex_buffer = visible.renderable->vertex_buffer, + .index_buffer = visible.renderable->index_buffer, + .index_count = visible.renderable->index_count, }; - submission.program = program; - submission.transform = std::span{identity}; + submission.program = visible.renderable->program; + submission.draw_state = visible.renderable->draw_state; + submission.transform = std::span{world_matrix}; renderer.submit(kMainView, submission); } diff --git a/tests/scene/scene_tests.cpp b/tests/scene/scene_tests.cpp new file mode 100644 index 0000000..c2017cc --- /dev/null +++ b/tests/scene/scene_tests.cpp @@ -0,0 +1,88 @@ +#include "engine/scene/scene.hpp" + +#include +#include + +int main() { + using namespace render; + + scene::Scene graph; + + const scene::SceneNodeId root = graph.create_node("root"); + const scene::SceneNodeId child = graph.create_node("child"); + const scene::SceneNodeId grandchild = graph.create_node("grandchild"); + + assert(graph.is_alive(root)); + assert(graph.node_count() == 3); + + assert(graph.set_parent(child, root, scene::ReparentPolicy::KeepLocalTransform)); + assert(graph.set_parent(grandchild, child, scene::ReparentPolicy::KeepLocalTransform)); + assert(!graph.set_parent(root, grandchild)); + + const std::vector* root_children = graph.children(root); + assert(root_children != nullptr); + assert(root_children->size() == 1); + assert(root_children->front() == child); + + core::Transform root_transform{}; + root_transform.translation = {10.0F, 0.0F, 0.0F}; + assert(graph.set_local_transform(root, root_transform)); + + core::Transform child_transform{}; + child_transform.translation = {0.0F, 5.0F, 0.0F}; + assert(graph.set_local_transform(child, child_transform)); + + graph.update_world_transforms(); + const core::Transform* grandchild_world = graph.world_transform(grandchild); + assert(grandchild_world != nullptr); + assert(core::nearly_equal(grandchild_world->translation.x, 10.0F)); + assert(core::nearly_equal(grandchild_world->translation.y, 5.0F)); + + assert(graph.clear_parent(child, scene::ReparentPolicy::KeepWorldTransform)); + graph.update_world_transforms(); + const core::Transform* child_local = graph.local_transform(child); + assert(child_local != nullptr); + assert(core::nearly_equal(child_local->translation.x, 10.0F)); + assert(core::nearly_equal(child_local->translation.y, 5.0F)); + + scene::CameraComponent camera{}; + assert(graph.set_camera(root, camera)); + assert(graph.set_active_camera(root)); + const std::optional camera_view = graph.build_camera_view(16.0F / 9.0F); + assert(camera_view.has_value()); + + scene::LightComponent light{}; + light.type = scene::LightType::Point; + light.range = 3.0F; + assert(graph.set_light(child, light)); + + scene::RenderableComponent renderable{}; + renderable.index_count = 6; + renderable.layer_mask = 0x1u; + assert(graph.set_renderable(grandchild, renderable)); + + const auto default_visible = graph.collect_visible_renderables(0x2u); + assert(default_visible.empty()); + + scene::NodeVisibility visible{}; + visible.layer_mask = 0x1u; + assert(graph.set_visibility(grandchild, visible)); + const auto filtered_visible = graph.collect_visible_renderables(0x1u); + assert(filtered_visible.size() == 1); + + assert(graph.set_debug_name(child, "duplicate")); + assert(graph.set_debug_name(grandchild, "duplicate")); + const auto duplicates = graph.find_nodes_by_name("duplicate"); + assert(duplicates.size() == 2); + + assert(graph.destroy_node(child)); + assert(!graph.is_alive(child)); + assert(!graph.is_alive(grandchild)); + + const scene::SceneNodeId recycled = graph.create_node("recycled"); + assert(recycled.index == child.index); + assert(recycled.generation != child.generation); + assert(!graph.is_alive(child)); + + return 0; +}