diff --git a/common/math/Bounds.cpp b/common/math/Bounds.cpp index ac017be93..a9890fbf9 100644 --- a/common/math/Bounds.cpp +++ b/common/math/Bounds.cpp @@ -16,6 +16,9 @@ // /////////////////////////////////////////////////////////////////////////////// +#include +#include + #include "Bounds.hpp" namespace nexo::math { @@ -71,4 +74,20 @@ namespace nexo::math { out.max = wc + we; return out; } + + static float maxAxisScale(const glm::mat4& M) + { + const float sx = glm::length(glm::vec3(M[0])); // column 0 + const float sy = glm::length(glm::vec3(M[1])); // column 1 + const float sz = glm::length(glm::vec3(M[2])); // column 2 + return std::max(sx, std::max(sy, sz)); + } + + BSphere sphereTransform(const BSphere& s, const glm::mat4& M) + { + BSphere out; + out.c = glm::vec3(M * glm::vec4(s.c, 1.0f)); + out.r = s.r * maxAxisScale(M); // conservative under non-uniform scale + return out; + } } diff --git a/common/math/Bounds.hpp b/common/math/Bounds.hpp index a5478d433..45e6d18e6 100644 --- a/common/math/Bounds.hpp +++ b/common/math/Bounds.hpp @@ -99,4 +99,6 @@ namespace nexo::math { * @return World-space AABB that encloses the transformed local box. */ AABB aabbTransform(const AABB& local, const glm::mat4& M); + + BSphere sphereTransform(const BSphere& s, const glm::mat4& M); } diff --git a/editor/src/utils/ScenePreview.cpp b/editor/src/utils/ScenePreview.cpp index 385cb4be5..4d1b2b3ac 100644 --- a/editor/src/utils/ScenePreview.cpp +++ b/editor/src/utils/ScenePreview.cpp @@ -12,153 +12,153 @@ // /////////////////////////////////////////////////////////////////////////////// -#include - #include "CameraFactory.hpp" #include "LightFactory.hpp" #include "Nexo.hpp" #include "ScenePreview.hpp" #include "components/Camera.hpp" -#include "components/MaterialComponent.hpp" +#include "components/Parent.hpp" +#include "math/Bounds.hpp" namespace nexo::editor::utils { - float computeBoundingSphereRadius(const components::TransformComponent &objectTransform) - { - const float halfX = objectTransform.size.x * 0.5f; - const float halfY = objectTransform.size.y * 0.5f; - const float halfZ = objectTransform.size.z * 0.5f; - return glm::max(halfX, glm::max(halfY, halfZ)); + namespace constants { + // Default bounding sphere + constexpr float kDefaultBoundingSpherePadding = 0.8f; + + // Camera setup + constexpr float kFramePadding = 1.12f; + constexpr float kDefaultFov = 45.0f; + constexpr glm::vec3 kCamThumbnailDir = glm::vec3(-0.321521f, -0.229658f, -0.91863f); // = normalize({-0.35f, -0.25f, -1.0f}) + constexpr float kPlanePadding = 1.3f; + + // Light setup + constexpr glm::vec3 kAmbientLightColor = glm::vec3(0.5f, 0.5f, 0.5f); + constexpr glm::vec3 kDirectionalLightDir = glm::vec3(0.2f, -1.0f, -0.3f); + constexpr glm::vec3 kSpotLightPos = glm::vec3(0.0f, 2.0f, -5.0f); + constexpr float kSpotLightOuterMargin = 2.5f; + constexpr glm::vec3 kSpotLightDir = glm::vec3(0.0f, -1.0f, 0.0f); } - float computeSpotlightHalfAngle(const components::TransformComponent &objectTransform, - const glm::vec3 &lightPosition) + /** + * @brief Compute the world-space bounding sphere of an entity. + * + * Prefers the model’s precomputed root bounding sphere if the entity has a RootComponent, + * conservatively transformed to world space (center transformed by world matrix, radius scaled + * by the max axis scale). If no model is present, a fallback sphere is synthesized from the + * entity’s TransformComponent::size (scaled by 0.8) and transformed to world. + * + * @param entity The entity whose bounds are requested. + * @return math::BSphere The world-space bounding sphere (never empty). + */ + static math::BSphere getWorldSphere(const ecs::Entity entity) { - const float radius = computeBoundingSphereRadius(objectTransform); - const float distance = glm::length(objectTransform.pos - lightPosition); - // Prevent division by zero - if (distance < 0.001f) return glm::radians(15.0f); - return atanf(radius / distance); - } + const auto& transform = Application::m_coordinator->getComponent(entity); + const glm::mat4 worldMatrix = transform.worldMatrix; - static ecs::Entity copyEntity(const ecs::Entity entity) - { - // TODO: Deep copy components instead of shallow copy - // const ecs::Entity entityCopy = Application::m_coordinator->createEntity(); - // const auto staticMeshCopy = - // Application::m_coordinator->getComponent(entity); const auto materialCopy = - // Application::m_coordinator->getComponent(entity); const auto - // &transformComponentBase = - // Application::m_coordinator->getComponent(entity); - // components::TransformComponent transformComponent; - // transformComponent.pos = {0.0f, 0.0f, -transformComponentBase.size.z * 2.0f}; - // transformComponent.quat = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); - // transformComponent.size = transformComponentBase.size; - // Application::m_coordinator->addComponent(entityCopy, staticMeshCopy); - // Application::m_coordinator->addComponent(entityCopy, materialCopy); - // Application::m_coordinator->addComponent(entityCopy, transformComponent); - // return entityCopy; - return Application::m_coordinator->duplicateEntity(entity); + if (const auto root = Application::m_coordinator->tryGetComponent(entity)) { + if (auto model = root->get().modelRef.lock()) { + return sphereTransform(model->rootSphere, worldMatrix); + } + } + + // Use transform size to compute a basic bounding sphere if the entity is not a model + const glm::vec3 half = constants::kDefaultBoundingSpherePadding * transform.size; + math::BSphere bsLocal; + bsLocal.c = glm::vec3(0.0f); + bsLocal.r = glm::length(half); + return sphereTransform(bsLocal, worldMatrix); } /** - * @brief Computes a default camera position for an entity based on its transform component. + * @brief Compute the half-angle for a spotlight that tightly covers an entity. * - * This function calculates a camera position that provides a good view of the entity - * by using its size to determine an appropriate distance and applying fixed yaw and pitch offsets. - * If the entity does not have a model component or if no vertices are available, this legacy method is used. + * Uses the entity’s world-space bounding sphere radius and the distance from light to the + * entity’s position to derive the required half-angle: atan(radius / distance). A minimum + * distance guard avoids divide-by-zero and returns a reasonable fallback. * - * @param entity The ECS entity for which to compute the camera position. - * @return glm::vec3 The computed camera position in world space. + * @param entity The target entity (bounds and transform are read). + * @param lightPosition The spotlight world position. + * @return float The spotlight half-angle in radians. + * + * @note The returned angle already matches GL/lighting conventions (radians). */ - glm::vec3 computeLegacyCameraPosition(const ecs::Entity entity) + static float computeSpotlightHalfAngle(ecs::Entity entity, + const glm::vec3 &lightPosition) { - const auto &transformComponentBase = + const float radius = getWorldSphere(entity).r; + const auto &transformComponent = Application::m_coordinator->getComponent(entity); - // If no vertices are available, use the transform component's size to compute the camera position - float distance = transformComponentBase.size.z * 3.0f; - - float defaultYawDeg = 30.0f; // horizontal offset - float defaultPitchDeg = -20.0f; // vertical offset - - float defaultYaw = glm::radians(defaultYawDeg); - float defaultPitch = glm::radians(defaultPitchDeg); - - glm::vec3 targetPos = transformComponentBase.pos; - - glm::vec3 initialOffset = {0.0f, 0.0f, distance}; - - glm::quat qYaw = glm::angleAxis(defaultYaw, glm::vec3(0, 1, 0)); - - glm::vec3 rightAxis = glm::normalize(glm::cross(glm::vec3(0, 1, 0), initialOffset)); - if (glm::length(rightAxis) < 0.001f) // Fallback if the vector is degenerate. - rightAxis = glm::vec3(1, 0, 0); - glm::quat qPitch = glm::angleAxis(defaultPitch, rightAxis); - - glm::quat incrementalRotation = qYaw * qPitch; - - glm::vec3 newOffset = incrementalRotation * initialOffset; - newOffset = glm::normalize(newOffset) * distance; - - glm::vec3 cameraPos = targetPos + newOffset; - std::cout << "Camera position computed: " << cameraPos.x << ", " << cameraPos.y << ", " << cameraPos.z - << std::endl; - return cameraPos; + const float distance = glm::length(transformComponent.pos - lightPosition); + // Prevent division by zero + if (distance < 0.001f) + return glm::radians(15.0f); + return atanf(radius / distance); } - glm::vec3 computeCameraPosition(const ecs::Entity entity, const float verticalFovDeg, const float aspectRatio, - const glm::vec3 &camForward = glm::vec3(0, 0, -1)) + /** + * @brief Fit a perspective camera to a world-space bounding sphere. + * + * Computes a camera position along a fixed viewing direction so that the entire sphere + * fits within both the vertical and horizontal FOV. Also produces tight near/far planes. + * A small padding factor is applied so the object doesn’t touch the viewport edges. + * + * @param bs World-space bounding sphere (center/radius in world units). + * @param aspect Viewport aspect ratio (width / height). + * @param outCamPos [out] Computed camera world position. + * @param outNearPlane [out] Suggested near plane distance. + * @param outFarPlane [out] Suggested far plane distance. + * + * @details + * The required distance is d = max( R / tan(fovy/2), R / tan(fovx/2) ), where + * R is the padded radius and fovx = 2 * atan( tan(fovy/2) * aspect ). + * The camera is placed at C - d * camDir, where C is the sphere center. + * + * @note The viewing direction is a fixed 3/4 view for consistent thumbnails. + * @warning near/far are chosen tightly around the sphere to improve depth precision; + * clamp near to a small epsilon to avoid clipping at the camera. + */ + static void fitCameraToBoundingSphere(const math::BSphere& bs, + float aspect, + glm::vec3 &outCamPos, + float &outNearPlane, + float &outFarPlane) { - const auto &modelComponent = Application::m_coordinator->tryGetComponent(entity); - if (!modelComponent) { - LOG(NEXO_ERROR, "Entity {} does not have model component, using default camera position computation", - entity); - return computeLegacyCameraPosition(entity); - } - // TODO: Create get vertices method for the model - // const auto vertices = modelComponent.model.getVertices(); - const std::vector &vertices = {}; - - if (vertices.empty()) { - LOG(NEXO_ERROR, "No vertices available for entity {}, using default camera position computation", entity); - return computeLegacyCameraPosition(entity); - } - // 1) Find AABB min/max - glm::vec3 vMin{std::numeric_limits::infinity(), std::numeric_limits::infinity(), - std::numeric_limits::infinity()}; - glm::vec3 vMax{-std::numeric_limits::infinity(), -std::numeric_limits::infinity(), - -std::numeric_limits::infinity()}; - - for (const auto &v : vertices) { - vMin.x = std::min(vMin.x, v.x); - vMin.y = std::min(vMin.y, v.y); - vMin.z = std::min(vMin.z, v.z); - vMax.x = std::max(vMax.x, v.x); - vMax.y = std::max(vMax.y, v.y); - vMax.z = std::max(vMax.z, v.z); - } + const float fovy = glm::radians(constants::kDefaultFov); + const float fovx = 2.0f * std::atan(std::tan(fovy * 0.5f) * aspect); - // 2) Compute center & half‐extents - const glm::vec3 center{(vMin.x + vMax.x) * 0.5f, (vMin.y + vMax.y) * 0.5f, (vMin.z + vMax.z) * 0.5f}; - const glm::vec3 extents{(vMax.x - vMin.x) * 0.5f, (vMax.y - vMin.y) * 0.5f, (vMax.z - vMin.z) * 0.5f}; + const float radius = bs.r * constants::kFramePadding; + const float dV = radius / std::tan(fovy * 0.5f); + const float dH = radius / std::tan(fovx * 0.5f); + const float d = std::max(dV, dH); - // 3) Compute half‐angles in radians - constexpr float deg2rad = std::numbers::pi_v / 180.0f; - const float halfVFovRad = 0.5f * verticalFovDeg * deg2rad; - const float halfHFovRad = std::atan(std::tan(halfVFovRad) * aspectRatio); + const glm::vec3 camPos = bs.c - d * constants::kCamThumbnailDir; - // 4) Compute distances needed to fit height & width - const float dVert = extents.y / std::tan(halfVFovRad); - const float dHoriz = extents.x / std::tan(halfHFovRad); - const float distance = std::max(dVert, dHoriz); + const float nearP = std::max(0.01f, d - radius * constants::kPlanePadding); + const float farP = d + radius * constants::kPlanePadding; - // 5) Position camera: move back from center along the NEGATIVE of the forward vector - const glm::vec3 forwardN = normalize(camForward); - return center - forwardN * distance; + outCamPos = camPos; + outNearPlane = nearP; + outFarPlane = farP; } - static ecs::Entity createPreviewCamera(const scene::SceneId sceneId, const ecs::Entity entity, + /** + * @brief Create and register a preview camera that frames an entity for thumbnail rendering. + * + * Allocates a framebuffer with color, ID, and depth attachments, computes the entity’s + * world-space bounding sphere, fits a camera to it (position and near/far), and orients + * the camera to look at the sphere center. The camera is added to the given editor scene. + * + * @param sceneId Target editor scene ID to receive the camera. + * @param entityCopy The (possibly duplicated) entity to frame in the preview. + * @param previewSize Preview resolution in pixels (x = width, y = height). + * @param clearColor Clear color applied to the camera’s framebuffer. + * @return ecs::Entity The created camera entity ID. + * + * @note Uses a fixed vertical FOV of 45° for consistent previews. + */ + static ecs::Entity createPreviewCamera(const scene::SceneId sceneId, const ecs::Entity entityCopy, const glm::vec2 &previewSize, const glm::vec4 &clearColor) { @@ -167,52 +167,58 @@ namespace nexo::editor::utils { framebufferSpecs.attachments = {renderer::NxFrameBufferTextureFormats::RGBA8, renderer::NxFrameBufferTextureFormats::RED_INTEGER, renderer::NxFrameBufferTextureFormats::Depth}; - framebufferSpecs.width = static_cast(previewSize.x); - framebufferSpecs.height = static_cast(previewSize.y); - const auto &transformComponentBase = - Application::m_coordinator->getComponent(entity); - const auto &transformComponent = - Application::m_coordinator->getComponent(entityCopy); + framebufferSpecs.width = static_cast(previewSize.x); + framebufferSpecs.height = static_cast(previewSize.y); const auto framebuffer = renderer::NxFramebuffer::create(framebufferSpecs); - const glm::vec3 cameraPos = computeCameraPosition(entity, 45.0f, previewSize.x / previewSize.y, - transformComponentBase.pos - transformComponent.pos); + const math::BSphere worldSphere = getWorldSphere(entityCopy); + + const float aspect = static_cast(framebufferSpecs.width) / float(std::max(1u, framebufferSpecs.height)); + glm::vec3 camPos; + float farPlane; + float nearPlane; + fitCameraToBoundingSphere(worldSphere, aspect, camPos, nearPlane, farPlane); const ecs::Entity cameraId = CameraFactory::createPerspectiveCamera( - cameraPos, framebufferSpecs.width, framebufferSpecs.height, framebuffer, clearColor); + camPos, framebufferSpecs.width, framebufferSpecs.height, framebuffer, clearColor, constants::kDefaultFov, nearPlane, farPlane); - auto &cameraTransform = Application::m_coordinator->getComponent(cameraId); - cameraTransform.pos = cameraPos; - auto &cameraComponent = Application::m_coordinator->getComponent(cameraId); - cameraComponent.render = true; + Application::m_coordinator->getComponent(cameraId).render = true; - const glm::vec3 newFront = glm::normalize(transformComponent.pos - cameraPos); - cameraTransform.quat = glm::normalize(glm::quatLookAt(newFront, glm::vec3(0.0f, 1.0f, 0.0f))); + const glm::vec3 forward = glm::normalize(worldSphere.c - camPos); + auto& cameraTransform = Application::m_coordinator->getComponent(cameraId); + cameraTransform.quat = glm::normalize(glm::quatLookAt(forward, glm::vec3(0,1,0))); - components::PerspectiveCameraTarget cameraTarget; - cameraTarget.targetEntity = entityCopy; - cameraTarget.distance = transformComponentBase.size.z * 2.0f; - Application::m_coordinator->addComponent(cameraId, cameraTarget); app.getSceneManager().getScene(sceneId).addEntity(cameraId); return cameraId; } + /** + * @brief Add basic lighting to a preview scene for consistent, readable thumbnails. + * + * Inserts the target entity into the scene, then creates: + * - an ambient light, + * - a directional light, + * - a spotlight with cone sized to cover the entity via its bounding sphere. + * + * @param sceneId The editor scene receiving the lights. + * @param entityCopy The entity instance to be lit in the preview scene. + * + * @note The spotlight’s inner/outer cones include a small angular margin to avoid edge clipping. + */ static void setupPreviewLights(const scene::SceneId sceneId, const ecs::Entity entityCopy) { auto &app = getApp(); - const auto &transformComponent = - Application::m_coordinator->getComponent(entityCopy); app.getSceneManager().getScene(sceneId).addEntity(entityCopy); - const ecs::Entity ambientLight = LightFactory::createAmbientLight({0.5f, 0.5f, 0.5f}); + const ecs::Entity ambientLight = LightFactory::createAmbientLight(constants::kAmbientLightColor); app.getSceneManager().getScene(sceneId).addEntity(ambientLight); - const ecs::Entity directionalLight = LightFactory::createDirectionalLight({0.2f, -1.0f, -0.3f}); + const ecs::Entity directionalLight = LightFactory::createDirectionalLight(constants::kDirectionalLightDir); app.getSceneManager().getScene(sceneId).addEntity(directionalLight); - const float spotLightHalfAngle = utils::computeSpotlightHalfAngle(transformComponent, {0.0, 2.0f, -5.0f}); - constexpr float margin = glm::radians(2.5f); - const ecs::Entity spotLight = LightFactory::createSpotLight( - {0.0f, 2.0f, -5.0f}, {0.0f, -1.0f, 0.0f}, {1.0f, 1.0f, 1.0f}, 0.0900000035F, 0.0320000015F, + const float spotLightHalfAngle = computeSpotlightHalfAngle(entityCopy, constants::kSpotLightPos); + constexpr float margin = glm::radians(constants::kSpotLightOuterMargin); + const ecs::Entity spotLight = LightFactory::createSpotLight( + constants::kSpotLightPos, constants::kSpotLightDir, {1.0f, 1.0f, 1.0f}, 0.0900000035F, 0.0320000015F, glm::cos(spotLightHalfAngle), glm::cos(spotLightHalfAngle + margin)); app.getSceneManager().getScene(sceneId).addEntity(spotLight); } @@ -224,9 +230,9 @@ namespace nexo::editor::utils { out.sceneId = app.getSceneManager().createEditorScene(uniqueSceneName); - out.entityCopy = copyEntity(entity); + out.entityCopy = Application::m_coordinator->duplicateEntity(entity); - out.cameraId = createPreviewCamera(out.sceneId, entity, out.entityCopy, previewSize, clearColor); + out.cameraId = createPreviewCamera(out.sceneId, out.entityCopy, previewSize, clearColor); setupPreviewLights(out.sceneId, out.entityCopy); out.sceneGenerated = true; diff --git a/editor/src/utils/ScenePreview.hpp b/editor/src/utils/ScenePreview.hpp index 9099f8507..8a2c2c88b 100644 --- a/editor/src/utils/ScenePreview.hpp +++ b/editor/src/utils/ScenePreview.hpp @@ -34,40 +34,20 @@ namespace nexo::editor::utils { }; /** - * @brief Computes an approximate bounding sphere radius for an object. + * @brief Generate a thumbnail preview scene for a single entity. * - * The radius is approximated by taking half the maximum dimension (x, y, or z) - * of the object's transform size. + * Creates a dedicated editor scene, duplicates the entity for isolation, spawns a camera + * that tightly frames the entity using a bounding-sphere fit, and sets up minimal lighting. + * Returns identifiers via the output struct for later rendering and resource management. * - * @param objectTransform The transform component of the object. - * @return float The computed bounding sphere radius. - */ - float computeBoundingSphereRadius(const components::TransformComponent &objectTransform); - - /** - * @brief Computes the half-angle of a spotlight based on an object's transform. - * - * Uses the bounding sphere radius of the object and the distance between the object - * and the light position to compute the half-angle of the spotlight. - * - * @param objectTransform The transform component of the object. - * @param lightPosition The position of the light. - * @return float The computed half-angle in radians. - */ - float computeSpotlightHalfAngle(const components::TransformComponent &objectTransform, - const glm::vec3 &lightPosition); - - /** - * @brief Generates a scene preview. - * - * Creates an editor scene with a copy of the given entity, a preview camera, and some default lights. - * The generated scene's ID, camera entity, and entity copy are stored in @p out. + * @param uniqueSceneName A unique name for the preview scene (used for scene registry). + * @param previewSize Preview resolution in pixels (x = width, y = height). + * @param entity The source entity to be duplicated into the preview scene. + * @param out [out] Filled with created scene/camera/entity IDs and a success flag. + * @param clearColor Clear color used by the preview camera. * - * @param uniqueSceneName A unique name for the preview scene. - * @param previewSize The size (width, height) of the preview. - * @param entity The entity to generate the preview from. - * @param out Output structure containing preview scene details. - * @param clearColor The clear color of the camera + * @note The duplicated entity avoids mutating original scene state during rendering. + * @warning Ensure scene lifecycle handles cleanup of the created entities/framebuffer as needed. */ void genScenePreview(const std::string &uniqueSceneName, const glm::vec2 &previewSize, ecs::Entity entity, ScenePreviewOut &out, const glm::vec4 &clearColor = {0.05f, 0.05f, 0.05f, 0.0f}); diff --git a/engine/src/Application.cpp b/engine/src/Application.cpp index 42a0bc86c..389204d44 100644 --- a/engine/src/Application.cpp +++ b/engine/src/Application.cpp @@ -330,7 +330,7 @@ namespace nexo { m_lightSystem->update(); m_renderCommandSystem->update(); m_renderBillboardSystem->update(); - m_aabbdebugSystem->update(); + //m_aabbdebugSystem->update(); if (!areVideoLoaded) { m_renderVideoSystem->update(); areVideoLoaded = true;