#include "SceneViewportOverlayBuilder.h" #include "Core/IEditorContext.h" #include "Core/ISceneManager.h" #include "SceneViewportOverlayHandleBuilder.h" #include "SceneViewportMath.h" #include #include #include #include #include #include #include #include #include namespace XCEngine { namespace Editor { namespace { bool CanBuildOverlayForGameObject(const Components::GameObject* gameObject) { return gameObject != nullptr && gameObject->GetTransform() != nullptr && gameObject->IsActiveInHierarchy(); } float ResolveCameraAspect( const Components::CameraComponent& camera, uint32_t viewportWidth, uint32_t viewportHeight) { const Math::Rect viewportRect = camera.GetViewportRect(); const float resolvedWidth = static_cast(viewportWidth) * (viewportRect.width > Math::EPSILON ? viewportRect.width : 1.0f); const float resolvedHeight = static_cast(viewportHeight) * (viewportRect.height > Math::EPSILON ? viewportRect.height : 1.0f); return resolvedHeight > Math::EPSILON ? resolvedWidth / resolvedHeight : 1.0f; } float ComputeWorldUnitsPerPixel( const SceneViewportOverlayData& overlay, const Math::Vector3& worldPoint, uint32_t viewportHeight) { if (!overlay.valid || viewportHeight <= 1u) { return 0.0f; } const Math::Vector3 cameraForward = overlay.cameraForward.Normalized(); const float depth = Math::Vector3::Dot(worldPoint - overlay.cameraPosition, cameraForward); if (depth <= Math::EPSILON) { return 0.0f; } return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / static_cast(viewportHeight); } void AppendWorldLine( SceneViewportOverlayFrameData& frameData, const Math::Vector3& startWorld, const Math::Vector3& endWorld, const Math::Color& color, float thicknessPixels, SceneViewportOverlayDepthMode depthMode) { SceneViewportOverlayLinePrimitive& line = frameData.worldLines.emplace_back(); line.startWorld = startWorld; line.endWorld = endWorld; line.color = color; line.thicknessPixels = thicknessPixels; line.depthMode = depthMode; } void AppendWorldSprite( SceneViewportOverlayFrameData& frameData, const Math::Vector3& worldPosition, const Math::Vector2& sizePixels, const Math::Color& tintColor, float sortDepth, uint64_t entityId, SceneViewportOverlaySpriteTextureKind textureKind, SceneViewportOverlayDepthMode depthMode) { if (entityId == 0 || sizePixels.x <= Math::EPSILON || sizePixels.y <= Math::EPSILON) { return; } SceneViewportOverlaySpritePrimitive& sprite = frameData.worldSprites.emplace_back(); sprite.worldPosition = worldPosition; sprite.sizePixels = sizePixels; sprite.tintColor = tintColor; sprite.sortDepth = sortDepth; sprite.entityId = entityId; sprite.textureKind = textureKind; sprite.depthMode = depthMode; } void AppendHandleRecord( SceneViewportOverlayFrameData& frameData, SceneViewportOverlayHandleKind kind, uint64_t handleId, uint64_t entityId, const Math::Vector3& worldPosition, const Math::Vector2& sizePixels, float sortDepth) { if (kind == SceneViewportOverlayHandleKind::None || handleId == 0 || entityId == 0 || sizePixels.x <= Math::EPSILON || sizePixels.y <= Math::EPSILON) { return; } SceneViewportOverlayHandleRecord& handleRecord = frameData.handleRecords.emplace_back(); handleRecord.kind = kind; handleRecord.handleId = handleId; handleRecord.entityId = entityId; handleRecord.shape = SceneViewportOverlayHandleShape::WorldRect; handleRecord.priority = Detail::kSceneViewportHandlePrioritySceneIcon; handleRecord.worldPosition = worldPosition; handleRecord.sizePixels = sizePixels; handleRecord.sortDepth = sortDepth; } void AppendSceneIconOverlay( SceneViewportOverlayFrameData& frameData, const SceneViewportOverlayData& overlay, uint32_t viewportWidth, uint32_t viewportHeight, const Components::GameObject& gameObject, const Math::Vector2& sizePixels, SceneViewportOverlaySpriteTextureKind textureKind) { const Components::TransformComponent* transform = gameObject.GetTransform(); if (transform == nullptr) { return; } const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint( overlay, static_cast(viewportWidth), static_cast(viewportHeight), transform->GetPosition()); if (!projectedPoint.visible) { return; } AppendWorldSprite( frameData, transform->GetPosition(), sizePixels, Math::Color::White(), projectedPoint.ndcDepth, gameObject.GetID(), textureKind, SceneViewportOverlayDepthMode::AlwaysOnTop); AppendHandleRecord( frameData, SceneViewportOverlayHandleKind::SceneIcon, gameObject.GetID(), gameObject.GetID(), transform->GetPosition(), sizePixels, projectedPoint.ndcDepth); } void AppendCameraFrustumOverlay( SceneViewportOverlayFrameData& frameData, const Components::CameraComponent& camera, const Components::GameObject& gameObject, uint32_t viewportWidth, uint32_t viewportHeight) { const Components::TransformComponent* transform = gameObject.GetTransform(); if (transform == nullptr) { return; } const Math::Vector3 position = transform->GetPosition(); const Math::Vector3 forward = transform->GetForward().Normalized(); const Math::Vector3 right = transform->GetRight().Normalized(); const Math::Vector3 up = transform->GetUp().Normalized(); if (forward.SqrMagnitude() <= Math::EPSILON || right.SqrMagnitude() <= Math::EPSILON || up.SqrMagnitude() <= Math::EPSILON) { return; } const float nearClip = (std::max)(camera.GetNearClipPlane(), 0.01f); const float farClip = (std::max)(camera.GetFarClipPlane(), nearClip + 0.01f); const float aspect = ResolveCameraAspect(camera, viewportWidth, viewportHeight); float nearHalfHeight = 0.0f; float nearHalfWidth = 0.0f; float farHalfHeight = 0.0f; float farHalfWidth = 0.0f; if (camera.GetProjectionType() == Components::CameraProjectionType::Perspective) { const float halfFovRadians = std::clamp(camera.GetFieldOfView(), 1.0f, 179.0f) * Math::DEG_TO_RAD * 0.5f; nearHalfHeight = std::tan(halfFovRadians) * nearClip; nearHalfWidth = nearHalfHeight * aspect; farHalfHeight = std::tan(halfFovRadians) * farClip; farHalfWidth = farHalfHeight * aspect; } else { const float halfHeight = (std::max)(camera.GetOrthographicSize(), 0.01f); const float halfWidth = halfHeight * aspect; nearHalfHeight = halfHeight; nearHalfWidth = halfWidth; farHalfHeight = halfHeight; farHalfWidth = halfWidth; } const Math::Vector3 nearCenter = position + forward * nearClip; const Math::Vector3 farCenter = position + forward * farClip; const std::array corners = {{ nearCenter + up * nearHalfHeight - right * nearHalfWidth, nearCenter + up * nearHalfHeight + right * nearHalfWidth, nearCenter - up * nearHalfHeight + right * nearHalfWidth, nearCenter - up * nearHalfHeight - right * nearHalfWidth, farCenter + up * farHalfHeight - right * farHalfWidth, farCenter + up * farHalfHeight + right * farHalfWidth, farCenter - up * farHalfHeight + right * farHalfWidth, farCenter - up * farHalfHeight - right * farHalfWidth }}; static constexpr std::array, 12> kFrustumEdges = {{ { 0u, 1u }, { 1u, 2u }, { 2u, 3u }, { 3u, 0u }, { 4u, 5u }, { 5u, 6u }, { 6u, 7u }, { 7u, 4u }, { 0u, 4u }, { 1u, 5u }, { 2u, 6u }, { 3u, 7u } }}; constexpr Math::Color kFrustumColor(1.0f, 1.0f, 1.0f, 1.0f); for (const auto& edge : kFrustumEdges) { AppendWorldLine( frameData, corners[edge.first], corners[edge.second], kFrustumColor, 1.6f, SceneViewportOverlayDepthMode::AlwaysOnTop); } } void AppendDirectionalLightOverlay( SceneViewportOverlayFrameData& frameData, const Components::GameObject& gameObject, const SceneViewportOverlayData& overlay, uint32_t viewportHeight) { const Components::TransformComponent* transform = gameObject.GetTransform(); if (transform == nullptr) { return; } const Math::Vector3 position = transform->GetPosition(); const Math::Vector3 lightDirection = (transform->GetForward() * -1.0f).Normalized(); const Math::Vector3 right = transform->GetRight().Normalized(); const Math::Vector3 up = transform->GetUp().Normalized(); if (lightDirection.SqrMagnitude() <= Math::EPSILON || right.SqrMagnitude() <= Math::EPSILON || up.SqrMagnitude() <= Math::EPSILON) { return; } const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(overlay, position, viewportHeight); if (worldUnitsPerPixel <= Math::EPSILON) { return; } constexpr Math::Color kDirectionalLightColor(1.0f, 0.92f, 0.24f, 1.0f); constexpr float kLineThickness = 1.8f; constexpr size_t kRingSegmentCount = 32; constexpr std::array kRayAngles = {{ 0.0f, Math::PI / 3.0f, Math::PI * 2.0f / 3.0f, Math::PI, Math::PI * 4.0f / 3.0f, Math::PI * 5.0f / 3.0f }}; const float ringRadius = worldUnitsPerPixel * 26.0f; const float ringOffset = worldUnitsPerPixel * 54.0f; const float innerRayRadius = ringRadius * 0.52f; const float rayLength = worldUnitsPerPixel * 96.0f; const Math::Vector3 ringCenter = position + lightDirection * ringOffset; for (size_t segmentIndex = 0; segmentIndex < kRingSegmentCount; ++segmentIndex) { const float angle0 = static_cast(segmentIndex) / static_cast(kRingSegmentCount) * Math::PI * 2.0f; const float angle1 = static_cast(segmentIndex + 1u) / static_cast(kRingSegmentCount) * Math::PI * 2.0f; const Math::Vector3 p0 = ringCenter + right * std::cos(angle0) * ringRadius + up * std::sin(angle0) * ringRadius; const Math::Vector3 p1 = ringCenter + right * std::cos(angle1) * ringRadius + up * std::sin(angle1) * ringRadius; AppendWorldLine( frameData, p0, p1, kDirectionalLightColor, kLineThickness, SceneViewportOverlayDepthMode::AlwaysOnTop); } AppendWorldLine( frameData, position, ringCenter, kDirectionalLightColor, kLineThickness, SceneViewportOverlayDepthMode::AlwaysOnTop); AppendWorldLine( frameData, ringCenter, ringCenter + lightDirection * rayLength, kDirectionalLightColor, kLineThickness, SceneViewportOverlayDepthMode::AlwaysOnTop); for (float angle : kRayAngles) { const Math::Vector3 rayStart = ringCenter + right * std::cos(angle) * innerRayRadius + up * std::sin(angle) * innerRayRadius; AppendWorldLine( frameData, rayStart, rayStart + lightDirection * rayLength, kDirectionalLightColor, kLineThickness, SceneViewportOverlayDepthMode::AlwaysOnTop); } } void AppendSceneObjectIconOverlays( SceneViewportOverlayFrameData& frameData, const Components::Scene& scene, const SceneViewportOverlayData& overlay, uint32_t viewportWidth, uint32_t viewportHeight) { constexpr Math::Vector2 kCameraIconSize(90.0f, 90.0f); constexpr Math::Vector2 kLightIconSize(100.0f, 100.0f); for (Components::CameraComponent* camera : scene.FindObjectsOfType()) { if (camera == nullptr || !camera->IsEnabled()) { continue; } Components::GameObject* gameObject = camera->GetGameObject(); if (!CanBuildOverlayForGameObject(gameObject)) { continue; } AppendSceneIconOverlay( frameData, overlay, viewportWidth, viewportHeight, *gameObject, kCameraIconSize, SceneViewportOverlaySpriteTextureKind::Camera); } for (Components::LightComponent* light : scene.FindObjectsOfType()) { if (light == nullptr || !light->IsEnabled()) { continue; } Components::GameObject* gameObject = light->GetGameObject(); if (!CanBuildOverlayForGameObject(gameObject)) { continue; } AppendSceneIconOverlay( frameData, overlay, viewportWidth, viewportHeight, *gameObject, kLightIconSize, SceneViewportOverlaySpriteTextureKind::Light); } } } // namespace SceneViewportOverlayFrameData SceneViewportOverlayBuilder::Build( IEditorContext& context, const SceneViewportOverlayData& overlay, uint32_t viewportWidth, uint32_t viewportHeight, const std::vector& selectedObjectIds) { SceneViewportOverlayFrameData frameData = {}; frameData.overlay = overlay; if (!overlay.valid || viewportWidth == 0u || viewportHeight == 0u) { return frameData; } const Components::Scene* scene = context.GetSceneManager().GetScene(); if (scene == nullptr) { return frameData; } AppendSceneObjectIconOverlays(frameData, *scene, overlay, viewportWidth, viewportHeight); for (uint64_t entityId : selectedObjectIds) { if (entityId == 0) { continue; } Components::GameObject* gameObject = context.GetSceneManager().GetEntity(entityId); if (!CanBuildOverlayForGameObject(gameObject)) { continue; } if (Components::CameraComponent* camera = gameObject->GetComponent(); camera != nullptr && camera->IsEnabled()) { AppendCameraFrustumOverlay(frameData, *camera, *gameObject, viewportWidth, viewportHeight); } if (Components::LightComponent* light = gameObject->GetComponent(); light != nullptr && light->IsEnabled() && light->GetLightType() == Components::LightType::Directional) { AppendDirectionalLightOverlay(frameData, *gameObject, overlay, viewportHeight); } } return frameData; } } // namespace Editor } // namespace XCEngine