- Updated README.md with modern project structure and features - Cleaned up Android build files (not needed for desktop engine) - Restructured as procedural 3D engine with ImGui integration - Based on Sascha Willems Vulkan framework with dynamic rendering - Added comprehensive build instructions and camera system docs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2735 lines
104 KiB
C++
2735 lines
104 KiB
C++
/*
|
|
* Procedural 3D Engine
|
|
* Copyright (c) 2025 Your Project
|
|
*
|
|
* This software is licensed under the MIT License.
|
|
* See LICENSE.md for full license information.
|
|
*
|
|
* Based on Vulkan examples by Sascha Willems (MIT License)
|
|
*/
|
|
|
|
#include <imgui.h>
|
|
#include "vulkanexamplebase.h"
|
|
#include "VulkanglTFModel.h"
|
|
#include <cmath>
|
|
#include <iostream>
|
|
|
|
#ifndef M_PI
|
|
#define M_PI 3.14159265358979323846
|
|
#endif
|
|
|
|
|
|
// Enhanced Camera System with Maya-style controls
|
|
class OrbitCamera {
|
|
public:
|
|
// Constructor
|
|
OrbitCamera(glm::vec3 initialPosition = glm::vec3(0.0f, 0.0f, 8.0f),
|
|
glm::vec3 focusPoint = glm::vec3(0.0f, 0.0f, 0.0f)) {
|
|
m_currentFocusPoint = focusPoint;
|
|
m_targetFocusPoint = focusPoint;
|
|
|
|
// Calculate initial spherical coordinates from position
|
|
glm::vec3 offset = initialPosition - focusPoint;
|
|
m_currentDistance = glm::length(offset);
|
|
m_targetDistance = m_currentDistance;
|
|
|
|
// Calculate initial angles
|
|
CartesianToSpherical(initialPosition, focusPoint, m_currentDistance, m_currentAzimuth, m_currentElevation);
|
|
m_targetAzimuth = m_currentAzimuth;
|
|
m_targetElevation = m_currentElevation;
|
|
|
|
m_currentPosition = initialPosition;
|
|
|
|
// Set default parameters
|
|
m_fov = 45.0f;
|
|
m_smoothingFactor = 0.1f;
|
|
m_orbitSensitivity = 0.5f;
|
|
m_panSensitivity = 0.00003f;
|
|
m_zoomSensitivity = 0.1f;
|
|
|
|
// Set constraints
|
|
m_minDistance = 0.5f;
|
|
m_maxDistance = 100.0f;
|
|
m_minElevation = -89.0f;
|
|
m_maxElevation = 89.0f;
|
|
|
|
m_viewMatrixDirty = true;
|
|
}
|
|
|
|
// Core camera operations
|
|
void Orbit(float deltaAzimuth, float deltaElevation, float deltaTime) {
|
|
// Apply input to target angles (convert degrees to radians)
|
|
m_targetAzimuth += glm::radians(deltaAzimuth * m_orbitSensitivity);
|
|
m_targetElevation += glm::radians(deltaElevation * m_orbitSensitivity);
|
|
ClampConstraints();
|
|
}
|
|
|
|
void Pan(float deltaX, float deltaY, float deltaTime) {
|
|
// Get camera's right and up vectors for screen-space panning (like legacy)
|
|
glm::mat4 viewMatrix = GetViewMatrix();
|
|
glm::vec3 right = glm::vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]);
|
|
glm::vec3 up = glm::vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]);
|
|
|
|
// Scale pan speed by distance to focus point for consistent behavior (like legacy)
|
|
float panScale = m_currentDistance * m_panSensitivity;
|
|
|
|
// Move focus point (like legacy)
|
|
glm::vec3 panOffset = right * deltaX * panScale + up * deltaY * panScale;
|
|
m_targetFocusPoint += panOffset;
|
|
}
|
|
|
|
void Zoom(float deltaDistance, float deltaTime) {
|
|
// Apply zoom with distance-based scaling for consistent behavior (like legacy)
|
|
m_targetDistance += deltaDistance * m_zoomSensitivity * m_currentDistance * 0.1f;
|
|
m_targetDistance = glm::clamp(m_targetDistance, m_minDistance, m_maxDistance);
|
|
}
|
|
|
|
void ZoomImmediate(float deltaDistance, float deltaTime) {
|
|
// Apply zoom with immediate response (no smoothing/velocity)
|
|
float zoomAmount = deltaDistance * m_zoomSensitivity * m_currentDistance * 0.1f;
|
|
|
|
// Set both target AND current immediately (no smoothing)
|
|
m_targetDistance += zoomAmount;
|
|
m_targetDistance = glm::clamp(m_targetDistance, m_minDistance, m_maxDistance);
|
|
m_currentDistance = m_targetDistance;
|
|
m_viewMatrixDirty = true;
|
|
}
|
|
|
|
void Update(float deltaTime) {
|
|
// Smooth interpolation towards target values
|
|
float lerpFactor = 1.0f - std::pow(m_smoothingFactor, deltaTime);
|
|
|
|
m_currentDistance = glm::mix(m_currentDistance, m_targetDistance, lerpFactor);
|
|
m_currentAzimuth = LerpAngle(m_currentAzimuth, m_targetAzimuth, lerpFactor);
|
|
m_currentElevation = glm::mix(m_currentElevation, m_targetElevation, lerpFactor);
|
|
m_currentFocusPoint = glm::mix(m_currentFocusPoint, m_targetFocusPoint, lerpFactor);
|
|
|
|
UpdatePosition();
|
|
}
|
|
|
|
// Matrix access
|
|
glm::mat4 GetViewMatrix() const {
|
|
if (m_viewMatrixDirty) {
|
|
m_viewMatrix = glm::lookAt(m_currentPosition, m_currentFocusPoint, glm::vec3(0.0f, 1.0f, 0.0f));
|
|
m_viewMatrixDirty = false;
|
|
}
|
|
return m_viewMatrix;
|
|
}
|
|
|
|
glm::mat4 GetProjectionMatrix(float aspectRatio, float nearPlane = 0.1f, float farPlane = 256.0f) const {
|
|
return glm::perspective(glm::radians(m_fov), aspectRatio, nearPlane, farPlane);
|
|
}
|
|
|
|
// Getters
|
|
glm::vec3 GetPosition() const { return m_currentPosition; }
|
|
glm::vec3 GetFocusPoint() const { return m_currentFocusPoint; }
|
|
float GetDistance() const { return m_currentDistance; }
|
|
float GetFOV() const { return m_fov; }
|
|
|
|
// Focus functionality
|
|
void SetFocusToSelection(glm::vec3 selectionCenter, float selectionRadius = 1.0f) {
|
|
m_targetFocusPoint = selectionCenter;
|
|
|
|
// Adjust distance based on selection size
|
|
if (selectionRadius > 0.0f) {
|
|
float recommendedDistance = selectionRadius * 3.0f; // Good viewing distance
|
|
recommendedDistance = glm::clamp(recommendedDistance, m_minDistance, m_maxDistance);
|
|
m_targetDistance = recommendedDistance;
|
|
}
|
|
|
|
std::cout << "OrbitCamera: Focus set to selection at ("
|
|
<< selectionCenter.x << ", " << selectionCenter.y << ", " << selectionCenter.z
|
|
<< ") with radius " << selectionRadius << std::endl;
|
|
}
|
|
|
|
void FrameAll(glm::vec3 sceneCenter, float sceneRadius) {
|
|
SetFocusToSelection(sceneCenter, sceneRadius);
|
|
}
|
|
|
|
// Configuration
|
|
void SetSensitivity(float orbitSens, float panSens, float zoomSens) {
|
|
m_orbitSensitivity = orbitSens;
|
|
m_panSensitivity = panSens;
|
|
m_zoomSensitivity = zoomSens;
|
|
}
|
|
|
|
void SetSmoothingFactor(float factor) {
|
|
m_smoothingFactor = glm::clamp(factor, 0.0f, 1.0f);
|
|
}
|
|
|
|
// Immediate (non-smoothed) operations for F key focus
|
|
void SetFocusPointImmediate(glm::vec3 focusPoint) {
|
|
m_targetFocusPoint = focusPoint;
|
|
m_currentFocusPoint = focusPoint; // Immediate change, no smoothing
|
|
m_viewMatrixDirty = true;
|
|
}
|
|
|
|
void SetDistanceImmediate(float distance) {
|
|
m_targetDistance = glm::clamp(distance, m_minDistance, m_maxDistance);
|
|
m_currentDistance = m_targetDistance; // Immediate change, no smoothing
|
|
UpdatePosition();
|
|
m_viewMatrixDirty = true;
|
|
}
|
|
|
|
void SetFocusToSelectionImmediate(glm::vec3 selectionCenter, float selectionRadius = 1.0f) {
|
|
SetFocusPointImmediate(selectionCenter);
|
|
|
|
// Adjust distance based on selection size
|
|
if (selectionRadius > 0.0f) {
|
|
float recommendedDistance = selectionRadius * 3.0f; // Good viewing distance
|
|
recommendedDistance = glm::clamp(recommendedDistance, m_minDistance, m_maxDistance);
|
|
SetDistanceImmediate(recommendedDistance);
|
|
}
|
|
|
|
std::cout << "OrbitCamera: Focus set immediately to selection at ("
|
|
<< selectionCenter.x << ", " << selectionCenter.y << ", " << selectionCenter.z
|
|
<< ") with radius " << selectionRadius << std::endl;
|
|
}
|
|
|
|
void FrameAllImmediate(glm::vec3 sceneCenter, float sceneRadius) {
|
|
SetFocusPointImmediate(sceneCenter);
|
|
|
|
// Calculate distance needed to frame the scene
|
|
float distance = sceneRadius / glm::tan(glm::radians(m_fov * 0.5f)) * 1.5f; // 1.5x padding
|
|
SetDistanceImmediate(distance);
|
|
}
|
|
|
|
void PanImmediate(float deltaX, float deltaY, float deltaTime) {
|
|
// Get camera's right and up vectors for screen-space panning (like Pan method)
|
|
glm::mat4 viewMatrix = GetViewMatrix();
|
|
glm::vec3 right = glm::vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]);
|
|
glm::vec3 up = glm::vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]);
|
|
|
|
// Scale pan speed by distance to focus point for consistent behavior
|
|
float panScale = m_currentDistance * m_panSensitivity;
|
|
|
|
// Calculate pan offset
|
|
glm::vec3 panOffset = right * deltaX * panScale + up * deltaY * panScale;
|
|
|
|
// Set both target AND current immediately (no smoothing)
|
|
m_targetFocusPoint += panOffset;
|
|
m_currentFocusPoint += panOffset;
|
|
m_viewMatrixDirty = true;
|
|
}
|
|
|
|
private:
|
|
// Current state (interpolated)
|
|
glm::vec3 m_currentPosition;
|
|
glm::vec3 m_currentFocusPoint;
|
|
float m_currentDistance;
|
|
float m_currentAzimuth; // Horizontal angle around focus point
|
|
float m_currentElevation; // Vertical angle (pitch)
|
|
|
|
// Target state (immediate input response)
|
|
glm::vec3 m_targetFocusPoint;
|
|
float m_targetDistance;
|
|
float m_targetAzimuth;
|
|
float m_targetElevation;
|
|
|
|
// Camera parameters
|
|
float m_fov; // Field of view in degrees
|
|
float m_smoothingFactor; // 0.0 = no smoothing, 1.0 = maximum smoothing
|
|
|
|
// Sensitivity settings
|
|
float m_orbitSensitivity;
|
|
float m_panSensitivity;
|
|
float m_zoomSensitivity;
|
|
|
|
// Constraints
|
|
float m_minDistance;
|
|
float m_maxDistance;
|
|
float m_minElevation; // In degrees
|
|
float m_maxElevation; // In degrees
|
|
|
|
// Cached matrices to avoid recalculation
|
|
mutable glm::mat4 m_viewMatrix;
|
|
mutable bool m_viewMatrixDirty;
|
|
|
|
// Internal methods
|
|
void UpdatePosition() {
|
|
m_currentPosition = SphericalToCartesian(m_currentDistance, m_currentAzimuth, m_currentElevation) + m_currentFocusPoint;
|
|
m_viewMatrixDirty = true;
|
|
}
|
|
|
|
void ClampConstraints() {
|
|
m_targetDistance = glm::clamp(m_targetDistance, m_minDistance, m_maxDistance);
|
|
m_targetElevation = glm::clamp(m_targetElevation, glm::radians(m_minElevation), glm::radians(m_maxElevation));
|
|
|
|
// Normalize azimuth to 0-2π range
|
|
while (m_targetAzimuth > 2.0f * M_PI) m_targetAzimuth -= 2.0f * M_PI;
|
|
while (m_targetAzimuth < 0.0f) m_targetAzimuth += 2.0f * M_PI;
|
|
}
|
|
|
|
glm::vec3 SphericalToCartesian(float distance, float azimuth, float elevation) const {
|
|
// Standard spherical coordinate conversion (input in radians)
|
|
float x = distance * glm::cos(elevation) * glm::cos(azimuth);
|
|
float y = distance * glm::sin(elevation);
|
|
float z = distance * glm::cos(elevation) * glm::sin(azimuth);
|
|
return glm::vec3(x, y, z);
|
|
}
|
|
|
|
void CartesianToSpherical(glm::vec3 position, glm::vec3 center, float& distance, float& azimuth, float& elevation) const {
|
|
glm::vec3 offset = position - center;
|
|
distance = glm::length(offset);
|
|
|
|
if (distance < 0.001f) {
|
|
// Very close to center, use default values
|
|
azimuth = 0.0f;
|
|
elevation = 0.0f;
|
|
return;
|
|
}
|
|
|
|
// Normalize offset for angle calculations
|
|
glm::vec3 normalized = offset / distance;
|
|
|
|
// Calculate azimuth (horizontal angle) - using atan for proper quadrant
|
|
azimuth = glm::atan(normalized.z, normalized.x);
|
|
|
|
// Calculate elevation (vertical angle) - use asin but clamp input to avoid NaN
|
|
float sinElevation = glm::clamp(normalized.y, -1.0f, 1.0f);
|
|
elevation = glm::asin(sinElevation);
|
|
}
|
|
|
|
float LerpAngle(float from, float to, float t) const {
|
|
// Handle angle wraparound for smooth interpolation (using radians)
|
|
float difference = to - from;
|
|
|
|
// Wrap difference to [-π, π] range
|
|
if (difference > M_PI) difference -= 2.0f * M_PI;
|
|
if (difference < -M_PI) difference += 2.0f * M_PI;
|
|
|
|
return from + difference * t;
|
|
}
|
|
};
|
|
|
|
|
|
// Procedural Geometry Generation
|
|
struct ProceduralVertex {
|
|
glm::vec3 position;
|
|
glm::vec3 normal;
|
|
glm::vec3 color;
|
|
};
|
|
|
|
struct ProceduralShape {
|
|
std::vector<ProceduralVertex> vertices;
|
|
std::vector<uint32_t> indices;
|
|
std::string name;
|
|
int type; // 0=cube, 1=sphere, 2=cylinder, 3=plane, 4=cone, 5=torus
|
|
|
|
// Shape parameters
|
|
struct {
|
|
float width = 2.0f, height = 2.0f, depth = 2.0f;
|
|
int subdivisions = 1;
|
|
float radius = 1.0f;
|
|
int segments = 16;
|
|
float majorRadius = 1.0f, minorRadius = 0.3f;
|
|
} params;
|
|
};
|
|
|
|
class ProceduralGeometry {
|
|
public:
|
|
static ProceduralShape generateCube(float width = 4.0f, float height = 4.0f, float depth = 4.0f) {
|
|
ProceduralShape shape;
|
|
shape.name = "Cube";
|
|
shape.type = 0;
|
|
shape.params.width = width;
|
|
shape.params.height = height;
|
|
shape.params.depth = depth;
|
|
|
|
float w = width * 0.5f;
|
|
float h = height * 0.5f;
|
|
float d = depth * 0.5f;
|
|
|
|
// Define 24 vertices (4 per face, 6 faces) with colors
|
|
shape.vertices = {
|
|
// Front face (red)
|
|
{{-w, -h, d}, {0, 0, 1}, {1, 0, 0}}, {{ w, -h, d}, {0, 0, 1}, {1, 0, 0}},
|
|
{{ w, h, d}, {0, 0, 1}, {1, 0, 0}}, {{-w, h, d}, {0, 0, 1}, {1, 0, 0}},
|
|
// Back face (green)
|
|
{{ w, -h, -d}, {0, 0, -1}, {0, 1, 0}}, {{-w, -h, -d}, {0, 0, -1}, {0, 1, 0}},
|
|
{{-w, h, -d}, {0, 0, -1}, {0, 1, 0}}, {{ w, h, -d}, {0, 0, -1}, {0, 1, 0}},
|
|
// Left face (blue)
|
|
{{-w, -h, -d}, {-1, 0, 0}, {0, 0, 1}}, {{-w, -h, d}, {-1, 0, 0}, {0, 0, 1}},
|
|
{{-w, h, d}, {-1, 0, 0}, {0, 0, 1}}, {{-w, h, -d}, {-1, 0, 0}, {0, 0, 1}},
|
|
// Right face (yellow)
|
|
{{ w, -h, d}, {1, 0, 0}, {1, 1, 0}}, {{ w, -h, -d}, {1, 0, 0}, {1, 1, 0}},
|
|
{{ w, h, -d}, {1, 0, 0}, {1, 1, 0}}, {{ w, h, d}, {1, 0, 0}, {1, 1, 0}},
|
|
// Top face (magenta)
|
|
{{-w, h, d}, {0, 1, 0}, {1, 0, 1}}, {{ w, h, d}, {0, 1, 0}, {1, 0, 1}},
|
|
{{ w, h, -d}, {0, 1, 0}, {1, 0, 1}}, {{-w, h, -d}, {0, 1, 0}, {1, 0, 1}},
|
|
// Bottom face (cyan)
|
|
{{-w, -h, -d}, {0, -1, 0}, {0, 1, 1}}, {{ w, -h, -d}, {0, -1, 0}, {0, 1, 1}},
|
|
{{ w, -h, d}, {0, -1, 0}, {0, 1, 1}}, {{-w, -h, d}, {0, -1, 0}, {0, 1, 1}}
|
|
};
|
|
|
|
// Define indices for 12 triangles (2 per face)
|
|
shape.indices = {
|
|
0,1,2, 0,2,3, // Front
|
|
4,5,6, 4,6,7, // Back
|
|
8,9,10, 8,10,11, // Left
|
|
12,13,14, 12,14,15, // Right
|
|
16,17,18, 16,18,19, // Top
|
|
20,21,22, 20,22,23 // Bottom
|
|
};
|
|
|
|
return shape;
|
|
}
|
|
|
|
static ProceduralShape generateSphere(float radius = 1.0f, int segments = 16) {
|
|
ProceduralShape shape;
|
|
shape.name = "Sphere";
|
|
shape.type = 1;
|
|
shape.params.radius = radius;
|
|
shape.params.segments = segments;
|
|
|
|
// Generate sphere vertices using spherical coordinates
|
|
for (int lat = 0; lat <= segments; ++lat) {
|
|
float theta = lat * M_PI / segments;
|
|
float sinTheta = sin(theta);
|
|
float cosTheta = cos(theta);
|
|
|
|
for (int lon = 0; lon <= segments; ++lon) {
|
|
float phi = lon * 2 * M_PI / segments;
|
|
float sinPhi = sin(phi);
|
|
float cosPhi = cos(phi);
|
|
|
|
glm::vec3 pos(radius * sinTheta * cosPhi, radius * cosTheta, radius * sinTheta * sinPhi);
|
|
glm::vec3 normal = glm::normalize(pos);
|
|
glm::vec3 color(0.8f, 0.8f, 0.8f); // Light gray color for sphere
|
|
|
|
shape.vertices.push_back({pos, normal, color});
|
|
}
|
|
}
|
|
|
|
// Generate indices
|
|
for (int lat = 0; lat < segments; ++lat) {
|
|
for (int lon = 0; lon < segments; ++lon) {
|
|
int first = lat * (segments + 1) + lon;
|
|
int second = first + segments + 1;
|
|
|
|
shape.indices.push_back(first);
|
|
shape.indices.push_back(second);
|
|
shape.indices.push_back(first + 1);
|
|
|
|
shape.indices.push_back(second);
|
|
shape.indices.push_back(second + 1);
|
|
shape.indices.push_back(first + 1);
|
|
}
|
|
}
|
|
|
|
return shape;
|
|
}
|
|
|
|
static ProceduralShape generatePlane(float width = 2.0f, float height = 2.0f, int subdivisions = 1) {
|
|
ProceduralShape shape;
|
|
shape.name = "Plane";
|
|
shape.type = 3;
|
|
shape.params.width = width;
|
|
shape.params.height = height;
|
|
shape.params.subdivisions = subdivisions;
|
|
|
|
float w = width * 0.5f;
|
|
float h = height * 0.5f;
|
|
|
|
// Simple quad for now
|
|
shape.vertices = {
|
|
{{-w, 0, -h}, {0, 1, 0}, {0.5f, 0.5f, 0.5f}}, // Gray plane
|
|
{{ w, 0, -h}, {0, 1, 0}, {0.5f, 0.5f, 0.5f}},
|
|
{{ w, 0, h}, {0, 1, 0}, {0.5f, 0.5f, 0.5f}},
|
|
{{-w, 0, h}, {0, 1, 0}, {0.5f, 0.5f, 0.5f}}
|
|
};
|
|
|
|
shape.indices = {0, 1, 2, 0, 2, 3};
|
|
|
|
return shape;
|
|
}
|
|
|
|
static ProceduralShape generateGrid(float size = 10.0f, int divisions = 10) {
|
|
ProceduralShape shape;
|
|
shape.name = "Grid";
|
|
shape.type = 6; // Grid type
|
|
shape.params.width = size;
|
|
shape.params.subdivisions = divisions;
|
|
|
|
float step = size / divisions;
|
|
float halfSize = size * 0.5f;
|
|
|
|
// Create grid lines (white grid)
|
|
glm::vec3 gridColor(1.0f, 1.0f, 1.0f);
|
|
for (int i = 0; i <= divisions; ++i) {
|
|
float pos = -halfSize + i * step;
|
|
|
|
// Horizontal lines
|
|
shape.vertices.push_back({{-halfSize, 0, pos}, {0, 1, 0}, gridColor});
|
|
shape.vertices.push_back({{ halfSize, 0, pos}, {0, 1, 0}, gridColor});
|
|
|
|
// Vertical lines
|
|
shape.vertices.push_back({{pos, 0, -halfSize}, {0, 1, 0}, gridColor});
|
|
shape.vertices.push_back({{pos, 0, halfSize}, {0, 1, 0}, gridColor});
|
|
}
|
|
|
|
// Generate indices for lines
|
|
for (int i = 0; i < (divisions + 1) * 4; i += 2) {
|
|
shape.indices.push_back(i);
|
|
shape.indices.push_back(i + 1);
|
|
}
|
|
|
|
return shape;
|
|
}
|
|
|
|
static ProceduralShape generateCone(float radius = 1.0f, float height = 2.0f, int segments = 16) {
|
|
ProceduralShape shape;
|
|
shape.name = "Cone";
|
|
shape.type = 4; // Cone type
|
|
shape.params.radius = radius;
|
|
shape.params.height = height;
|
|
shape.params.segments = segments;
|
|
|
|
// Add tip vertex (red cone tip)
|
|
shape.vertices.push_back({{0, height * 0.5f, 0}, {0, 1, 0}, {1.0f, 0.0f, 0.0f}});
|
|
|
|
// Add center vertex for base (dark red)
|
|
shape.vertices.push_back({{0, -height * 0.5f, 0}, {0, -1, 0}, {0.5f, 0.0f, 0.0f}});
|
|
|
|
// Generate base vertices
|
|
for (int i = 0; i <= segments; ++i) {
|
|
float angle = (float)i / segments * 2.0f * M_PI;
|
|
float x = cos(angle) * radius;
|
|
float z = sin(angle) * radius;
|
|
glm::vec3 color(0.8f, 0.2f, 0.2f); // Light red
|
|
|
|
// Base vertex
|
|
shape.vertices.push_back({{x, -height * 0.5f, z}, {0, -1, 0}, color});
|
|
|
|
// Side vertex (for side triangles)
|
|
glm::vec3 sideNormal = glm::normalize(glm::vec3(x, radius / height, z));
|
|
shape.vertices.push_back({{x, -height * 0.5f, z}, sideNormal, color});
|
|
}
|
|
|
|
// Generate indices
|
|
// Side triangles (tip to base edge)
|
|
for (int i = 0; i < segments; ++i) {
|
|
int baseStart = 2 + segments + 1;
|
|
shape.indices.push_back(0); // tip
|
|
shape.indices.push_back(baseStart + (i + 1) * 2);
|
|
shape.indices.push_back(baseStart + i * 2);
|
|
}
|
|
|
|
// Base triangles
|
|
for (int i = 0; i < segments; ++i) {
|
|
shape.indices.push_back(1); // center
|
|
shape.indices.push_back(2 + i);
|
|
shape.indices.push_back(2 + ((i + 1) % (segments + 1)));
|
|
}
|
|
|
|
return shape;
|
|
}
|
|
|
|
static ProceduralShape generateCylinder(float radius = 1.0f, float height = 2.0f, int segments = 16) {
|
|
ProceduralShape shape;
|
|
shape.name = "Cylinder";
|
|
shape.type = 5; // Cylinder type
|
|
shape.params.radius = radius;
|
|
shape.params.height = height;
|
|
shape.params.segments = segments;
|
|
|
|
float halfHeight = height * 0.5f;
|
|
|
|
// Add center vertices for caps (blue cylinder)
|
|
shape.vertices.push_back({{0, halfHeight, 0}, {0, 1, 0}, {0.0f, 0.0f, 1.0f}}); // top center
|
|
shape.vertices.push_back({{0, -halfHeight, 0}, {0, -1, 0}, {0.0f, 0.0f, 1.0f}}); // bottom center
|
|
|
|
// Generate side vertices (double for proper normals)
|
|
for (int i = 0; i <= segments; ++i) {
|
|
float angle = (float)i / segments * 2.0f * M_PI;
|
|
float x = cos(angle) * radius;
|
|
float z = sin(angle) * radius;
|
|
glm::vec3 normal = glm::normalize(glm::vec3(x, 0, z));
|
|
glm::vec3 color(0.2f, 0.4f, 1.0f); // Light blue
|
|
|
|
// Top vertices
|
|
shape.vertices.push_back({{x, halfHeight, z}, {0, 1, 0}, color}); // top cap
|
|
shape.vertices.push_back({{x, halfHeight, z}, normal, color}); // top side
|
|
|
|
// Bottom vertices
|
|
shape.vertices.push_back({{x, -halfHeight, z}, {0, -1, 0}, color}); // bottom cap
|
|
shape.vertices.push_back({{x, -halfHeight, z}, normal, color}); // bottom side
|
|
}
|
|
|
|
// Generate indices
|
|
for (int i = 0; i < segments; ++i) {
|
|
int topCapStart = 2;
|
|
int bottomCapStart = 4;
|
|
|
|
// Top cap triangles
|
|
shape.indices.push_back(0); // top center
|
|
shape.indices.push_back(topCapStart + ((i + 1) % (segments + 1)) * 4);
|
|
shape.indices.push_back(topCapStart + i * 4);
|
|
|
|
// Bottom cap triangles
|
|
shape.indices.push_back(1); // bottom center
|
|
shape.indices.push_back(bottomCapStart + i * 4);
|
|
shape.indices.push_back(bottomCapStart + ((i + 1) % (segments + 1)) * 4);
|
|
|
|
// Side quads (as two triangles)
|
|
int topSide1 = topCapStart + 1 + i * 4;
|
|
int topSide2 = topCapStart + 1 + ((i + 1) % (segments + 1)) * 4;
|
|
int bottomSide1 = bottomCapStart + 1 + i * 4;
|
|
int bottomSide2 = bottomCapStart + 1 + ((i + 1) % (segments + 1)) * 4;
|
|
|
|
// First triangle
|
|
shape.indices.push_back(topSide1);
|
|
shape.indices.push_back(bottomSide1);
|
|
shape.indices.push_back(topSide2);
|
|
|
|
// Second triangle
|
|
shape.indices.push_back(topSide2);
|
|
shape.indices.push_back(bottomSide1);
|
|
shape.indices.push_back(bottomSide2);
|
|
}
|
|
|
|
return shape;
|
|
}
|
|
|
|
static ProceduralShape generateTorus(float majorRadius = 1.0f, float minorRadius = 0.3f, int majorSegments = 16, int minorSegments = 8) {
|
|
ProceduralShape shape;
|
|
shape.name = "Torus";
|
|
shape.type = 6; // Torus type
|
|
shape.params.majorRadius = majorRadius;
|
|
shape.params.minorRadius = minorRadius;
|
|
shape.params.segments = majorSegments;
|
|
shape.params.subdivisions = minorSegments;
|
|
|
|
// Generate vertices
|
|
for (int i = 0; i <= majorSegments; ++i) {
|
|
float majorAngle = (float)i / majorSegments * 2.0f * M_PI;
|
|
float cosMajor = cos(majorAngle);
|
|
float sinMajor = sin(majorAngle);
|
|
|
|
for (int j = 0; j <= minorSegments; ++j) {
|
|
float minorAngle = (float)j / minorSegments * 2.0f * M_PI;
|
|
float cosMinor = cos(minorAngle);
|
|
float sinMinor = sin(minorAngle);
|
|
|
|
// Calculate position
|
|
float x = (majorRadius + minorRadius * cosMinor) * cosMajor;
|
|
float y = minorRadius * sinMinor;
|
|
float z = (majorRadius + minorRadius * cosMinor) * sinMajor;
|
|
|
|
// Calculate normal
|
|
glm::vec3 center(majorRadius * cosMajor, 0, majorRadius * sinMajor);
|
|
glm::vec3 position(x, y, z);
|
|
glm::vec3 normal = glm::normalize(position - center);
|
|
|
|
// Use orange color for torus
|
|
glm::vec3 color(1.0f, 0.5f, 0.0f);
|
|
|
|
shape.vertices.push_back({{x, y, z}, normal, color});
|
|
}
|
|
}
|
|
|
|
// Generate indices
|
|
for (int i = 0; i < majorSegments; ++i) {
|
|
for (int j = 0; j < minorSegments; ++j) {
|
|
int current = i * (minorSegments + 1) + j;
|
|
int next = ((i + 1) % (majorSegments + 1)) * (minorSegments + 1) + j;
|
|
|
|
// First triangle
|
|
shape.indices.push_back(current);
|
|
shape.indices.push_back(next);
|
|
shape.indices.push_back(current + 1);
|
|
|
|
// Second triangle
|
|
shape.indices.push_back(next);
|
|
shape.indices.push_back(next + 1);
|
|
shape.indices.push_back(current + 1);
|
|
}
|
|
}
|
|
|
|
return shape;
|
|
}
|
|
};
|
|
|
|
// Simple Scene Object for basic hierarchy
|
|
struct SceneObject {
|
|
std::string name;
|
|
std::string type;
|
|
std::string subtype; // For procedural shapes (Cube, Sphere, etc.)
|
|
bool visible = true;
|
|
|
|
// Simple geometry data (for procedural shapes)
|
|
std::vector<ProceduralVertex> vertices;
|
|
std::vector<uint32_t> indices;
|
|
glm::vec3 position = {0.0f, 0.0f, 0.0f}; // Place objects at world origin
|
|
glm::vec3 rotation = {0.0f, 0.0f, 0.0f};
|
|
glm::vec3 scale = {1.0f, 1.0f, 1.0f}; // Default unit scale
|
|
|
|
// Procedural shape parameters (for real-time editing)
|
|
struct ProceduralParams {
|
|
// Cube parameters
|
|
float cubeWidth = 2.0f;
|
|
float cubeHeight = 2.0f;
|
|
float cubeDepth = 2.0f;
|
|
int cubeSubdivisions = 1;
|
|
|
|
// Sphere parameters
|
|
float sphereRadius = 1.0f;
|
|
int sphereSegments = 16;
|
|
|
|
// Cylinder parameters
|
|
float cylinderRadius = 1.0f;
|
|
float cylinderHeight = 2.0f;
|
|
int cylinderSegments = 16;
|
|
|
|
// Cone parameters
|
|
float coneRadius = 1.0f;
|
|
float coneHeight = 2.0f;
|
|
int coneSegments = 16;
|
|
|
|
// Plane parameters
|
|
float planeWidth = 2.0f;
|
|
float planeHeight = 2.0f;
|
|
int planeSubdivisions = 1;
|
|
|
|
// Torus parameters
|
|
float torusMajorRadius = 1.0f;
|
|
float torusMinorRadius = 0.3f;
|
|
int torusMajorSegments = 16;
|
|
int torusMinorSegments = 8;
|
|
} proceduralParams;
|
|
|
|
// Persistent Vulkan buffers (created once, used multiple times)
|
|
vks::Buffer vertexBuffer;
|
|
vks::Buffer indexBuffer;
|
|
bool buffersCreated = false;
|
|
|
|
SceneObject(const std::string& objName, const std::string& objType = "Object")
|
|
: name(objName), type(objType) {}
|
|
|
|
// Cleanup buffers when object is destroyed
|
|
void destroyBuffers(vks::VulkanDevice* device) {
|
|
if (buffersCreated) {
|
|
vertexBuffer.destroy();
|
|
indexBuffer.destroy();
|
|
buffersCreated = false;
|
|
}
|
|
}
|
|
|
|
// Calculate bounding box center for camera focus
|
|
glm::vec3 getBoundingBoxCenter() const {
|
|
if (vertices.empty()) return position;
|
|
|
|
glm::vec3 minPos = vertices[0].position;
|
|
glm::vec3 maxPos = vertices[0].position;
|
|
|
|
for (const auto& vertex : vertices) {
|
|
minPos = glm::min(minPos, vertex.position);
|
|
maxPos = glm::max(maxPos, vertex.position);
|
|
}
|
|
|
|
// Apply object transformation
|
|
glm::vec3 center = (minPos + maxPos) * 0.5f;
|
|
center = position + center * scale; // Simple transform (rotation not included for simplicity)
|
|
|
|
return center;
|
|
}
|
|
|
|
// Calculate bounding radius for camera focus
|
|
float getBoundingRadius() const {
|
|
if (vertices.empty()) return 1.0f;
|
|
|
|
glm::vec3 center = getBoundingBoxCenter();
|
|
float maxRadius = 0.0f;
|
|
|
|
for (const auto& vertex : vertices) {
|
|
glm::vec3 transformedPos = position + vertex.position * scale;
|
|
float distance = glm::length(transformedPos - center);
|
|
maxRadius = std::max(maxRadius, distance);
|
|
}
|
|
|
|
return std::max(maxRadius, 0.5f); // Minimum radius of 0.5
|
|
}
|
|
|
|
// Regenerate geometry based on current procedural parameters
|
|
void regenerateGeometry() {
|
|
if (type != "Procedural") return;
|
|
|
|
ProceduralShape shape;
|
|
|
|
if (subtype == "Cube") {
|
|
shape = ProceduralGeometry::generateCube(proceduralParams.cubeWidth,
|
|
proceduralParams.cubeHeight,
|
|
proceduralParams.cubeDepth);
|
|
} else if (subtype == "Sphere") {
|
|
shape = ProceduralGeometry::generateSphere(proceduralParams.sphereRadius,
|
|
proceduralParams.sphereSegments);
|
|
} else if (subtype == "Cylinder") {
|
|
shape = ProceduralGeometry::generateCylinder(proceduralParams.cylinderRadius,
|
|
proceduralParams.cylinderHeight,
|
|
proceduralParams.cylinderSegments);
|
|
} else if (subtype == "Cone") {
|
|
shape = ProceduralGeometry::generateCone(proceduralParams.coneRadius,
|
|
proceduralParams.coneHeight,
|
|
proceduralParams.coneSegments);
|
|
} else if (subtype == "Plane") {
|
|
shape = ProceduralGeometry::generatePlane(proceduralParams.planeWidth,
|
|
proceduralParams.planeHeight,
|
|
proceduralParams.planeSubdivisions);
|
|
} else if (subtype == "Torus") {
|
|
shape = ProceduralGeometry::generateTorus(proceduralParams.torusMajorRadius,
|
|
proceduralParams.torusMinorRadius,
|
|
proceduralParams.torusMajorSegments,
|
|
proceduralParams.torusMinorSegments);
|
|
}
|
|
|
|
// Update geometry data
|
|
vertices = shape.vertices;
|
|
indices = shape.indices;
|
|
|
|
// Destroy existing buffers if they exist (to handle size changes)
|
|
if (buffersCreated) {
|
|
vertexBuffer.destroy();
|
|
indexBuffer.destroy();
|
|
}
|
|
|
|
// Mark buffers as needing recreation
|
|
buffersCreated = false;
|
|
}
|
|
};
|
|
|
|
// Simple scene management
|
|
struct SimpleSceneManager {
|
|
std::vector<SceneObject> objects;
|
|
int selectedIndex = -1;
|
|
vks::VulkanDevice* device = nullptr;
|
|
|
|
void addObject(const std::string& name, const std::string& type = "Procedural") {
|
|
objects.emplace_back(name, type);
|
|
selectedIndex = static_cast<int>(objects.size() - 1);
|
|
}
|
|
|
|
void addProceduralShape(const std::string& shapeName, const std::string& shapeType) {
|
|
std::string fullName = shapeName + " " + std::to_string(getObjectCount() + 1);
|
|
objects.emplace_back(fullName, "Procedural");
|
|
|
|
// Generate actual geometry based on shape type
|
|
SceneObject& newObj = objects.back();
|
|
newObj.subtype = shapeType; // Set the shape subtype (Cube, Sphere, etc.)
|
|
|
|
// Place all objects at world origin (0,0,0)
|
|
// Users can move them via transform controls in Inspector
|
|
newObj.position.x = 0.0f;
|
|
newObj.position.y = 0.0f;
|
|
newObj.position.z = 0.0f;
|
|
|
|
// Initialize procedural parameters based on shape type (use regenerateGeometry to create initial geometry)
|
|
newObj.regenerateGeometry();
|
|
|
|
// Create Vulkan buffers for the geometry (delayed until first render)
|
|
// createBuffersForObject(newObj);
|
|
|
|
selectedIndex = static_cast<int>(objects.size() - 1);
|
|
}
|
|
|
|
void createBuffersForObject(SceneObject& obj) {
|
|
// Enhanced validation
|
|
if (!device) {
|
|
std::cout << "Error: Device not available for buffer creation" << std::endl;
|
|
return;
|
|
}
|
|
|
|
if (obj.vertices.empty() || obj.indices.empty()) {
|
|
std::cout << "Error: Cannot create buffers for " << obj.name << " - geometry data is empty" << std::endl;
|
|
return;
|
|
}
|
|
|
|
if (obj.buffersCreated) {
|
|
std::cout << "Info: Buffers already created for " << obj.name << std::endl;
|
|
return;
|
|
}
|
|
|
|
// Validate geometry data size
|
|
if (obj.vertices.size() > 1000000 || obj.indices.size() > 3000000) {
|
|
std::cout << "Warning: Large geometry for " << obj.name << " - vertices: " << obj.vertices.size() << ", indices: " << obj.indices.size() << std::endl;
|
|
}
|
|
|
|
try {
|
|
std::cout << "Creating buffers for " << obj.name << " with " << obj.vertices.size() << " vertices and " << obj.indices.size() << " indices" << std::endl;
|
|
|
|
// Create vertex buffer
|
|
VkDeviceSize vertexBufferSize = obj.vertices.size() * sizeof(ProceduralVertex);
|
|
if (vertexBufferSize == 0) {
|
|
std::cout << "Error: Zero vertex buffer size for " << obj.name << std::endl;
|
|
return;
|
|
}
|
|
|
|
VkResult result = device->createBuffer(
|
|
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
|
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
|
&obj.vertexBuffer,
|
|
vertexBufferSize,
|
|
(void*)obj.vertices.data()
|
|
);
|
|
|
|
if (result != VK_SUCCESS) {
|
|
std::cout << "Failed to create vertex buffer for " << obj.name << " - VkResult: " << result << std::endl;
|
|
return;
|
|
}
|
|
|
|
// Validate vertex buffer creation
|
|
if (obj.vertexBuffer.buffer == VK_NULL_HANDLE) {
|
|
std::cout << "Error: Vertex buffer handle is null for " << obj.name << std::endl;
|
|
return;
|
|
}
|
|
|
|
// Create index buffer
|
|
VkDeviceSize indexBufferSize = obj.indices.size() * sizeof(uint32_t);
|
|
if (indexBufferSize == 0) {
|
|
std::cout << "Error: Zero index buffer size for " << obj.name << std::endl;
|
|
obj.vertexBuffer.destroy();
|
|
return;
|
|
}
|
|
|
|
result = device->createBuffer(
|
|
VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
|
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
|
&obj.indexBuffer,
|
|
indexBufferSize,
|
|
(void*)obj.indices.data()
|
|
);
|
|
|
|
if (result != VK_SUCCESS) {
|
|
std::cout << "Failed to create index buffer for " << obj.name << " - VkResult: " << result << std::endl;
|
|
obj.vertexBuffer.destroy();
|
|
return;
|
|
}
|
|
|
|
// Validate index buffer creation
|
|
if (obj.indexBuffer.buffer == VK_NULL_HANDLE) {
|
|
std::cout << "Error: Index buffer handle is null for " << obj.name << std::endl;
|
|
obj.vertexBuffer.destroy();
|
|
return;
|
|
}
|
|
|
|
obj.buffersCreated = true;
|
|
std::cout << "Successfully created buffers for " << obj.name << " (vertex: " << vertexBufferSize << " bytes, index: " << indexBufferSize << " bytes)" << std::endl;
|
|
}
|
|
catch (const std::exception& e) {
|
|
std::cout << "Exception creating buffers for " << obj.name << ": " << e.what() << std::endl;
|
|
obj.buffersCreated = false;
|
|
// Clean up any partially created buffers
|
|
if (obj.vertexBuffer.buffer != VK_NULL_HANDLE) {
|
|
obj.vertexBuffer.destroy();
|
|
}
|
|
if (obj.indexBuffer.buffer != VK_NULL_HANDLE) {
|
|
obj.indexBuffer.destroy();
|
|
}
|
|
}
|
|
}
|
|
|
|
void removeObject(int index) {
|
|
if (index >= 0 && index < objects.size()) {
|
|
// Clean up buffers before removing object
|
|
objects[index].destroyBuffers(device);
|
|
objects.erase(objects.begin() + index);
|
|
if (selectedIndex >= objects.size()) {
|
|
selectedIndex = static_cast<int>(objects.size() - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void clearScene() {
|
|
// Clean up all buffers before clearing
|
|
for (auto& obj : objects) {
|
|
obj.destroyBuffers(device);
|
|
}
|
|
objects.clear();
|
|
selectedIndex = -1;
|
|
}
|
|
|
|
int getObjectCount() const {
|
|
return static_cast<int>(objects.size());
|
|
}
|
|
|
|
// Selection management
|
|
void setSelectedIndex(int index) {
|
|
if (index >= -1 && index < static_cast<int>(objects.size())) {
|
|
selectedIndex = index;
|
|
}
|
|
}
|
|
|
|
int getSelectedIndex() const {
|
|
return selectedIndex;
|
|
}
|
|
|
|
SceneObject* getSelectedObject() {
|
|
if (selectedIndex >= 0 && selectedIndex < static_cast<int>(objects.size())) {
|
|
return &objects[selectedIndex];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool hasSelection() const {
|
|
return selectedIndex >= 0 && selectedIndex < static_cast<int>(objects.size());
|
|
}
|
|
|
|
void clearSelection() {
|
|
selectedIndex = -1;
|
|
}
|
|
} sceneManager;
|
|
|
|
// Options and values to display/toggle from the UI
|
|
struct UISettings {
|
|
bool displayModels = false;
|
|
bool displayBackground = false;
|
|
bool animateLight = false;
|
|
float lightSpeed = 0.25f;
|
|
std::array<float, 50> frameTimes{};
|
|
float frameTimeMin = 9999.0f, frameTimeMax = 0.0f;
|
|
float lightTimer = 0.0f;
|
|
bool showGrid = true;
|
|
float gridSize = 10.0f;
|
|
int gridDivisions = 10;
|
|
|
|
// Panel visibility settings (for Windows menu)
|
|
bool showViewportPanel = true;
|
|
bool showInspectorPanel = true;
|
|
bool showSceneHierarchyPanel = true;
|
|
bool showAssetBrowserPanel = true;
|
|
bool showConsolePanel = false; // Hidden by default
|
|
} uiSettings;
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// ImGUI class
|
|
// ----------------------------------------------------------------------------
|
|
class ImGUI {
|
|
private:
|
|
// Vulkan resources for rendering the UI
|
|
VkSampler sampler;
|
|
vks::Buffer vertexBuffer;
|
|
vks::Buffer indexBuffer;
|
|
int32_t vertexCount = 0;
|
|
int32_t indexCount = 0;
|
|
VkDeviceMemory fontMemory = VK_NULL_HANDLE;
|
|
VkImage fontImage = VK_NULL_HANDLE;
|
|
VkImageView fontView = VK_NULL_HANDLE;
|
|
VkPipelineCache pipelineCache;
|
|
VkPipelineLayout pipelineLayout;
|
|
VkPipeline pipeline;
|
|
VkDescriptorPool descriptorPool;
|
|
VkDescriptorSetLayout descriptorSetLayout;
|
|
VkDescriptorSet descriptorSet;
|
|
vks::VulkanDevice *device;
|
|
VkPhysicalDeviceDriverProperties driverProperties = {};
|
|
VulkanExampleBase *example;
|
|
ImGuiStyle vulkanStyle;
|
|
int selectedStyle = 0;
|
|
public:
|
|
// UI params are set via push constants
|
|
struct PushConstBlock {
|
|
glm::vec2 scale;
|
|
glm::vec2 translate;
|
|
} pushConstBlock;
|
|
|
|
ImGUI(VulkanExampleBase *example) : example(example)
|
|
{
|
|
device = example->vulkanDevice;
|
|
ImGui::CreateContext();
|
|
|
|
//SRS - Set ImGui font and style scale factors to handle retina and other HiDPI displays
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.FontGlobalScale = example->ui.scale;
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
style.ScaleAllSizes(example->ui.scale);
|
|
};
|
|
|
|
~ImGUI()
|
|
{
|
|
ImGui::DestroyContext();
|
|
// Release all Vulkan resources required for rendering imGui
|
|
vertexBuffer.destroy();
|
|
indexBuffer.destroy();
|
|
vkDestroyImage(device->logicalDevice, fontImage, nullptr);
|
|
vkDestroyImageView(device->logicalDevice, fontView, nullptr);
|
|
vkFreeMemory(device->logicalDevice, fontMemory, nullptr);
|
|
vkDestroySampler(device->logicalDevice, sampler, nullptr);
|
|
vkDestroyPipelineCache(device->logicalDevice, pipelineCache, nullptr);
|
|
vkDestroyPipeline(device->logicalDevice, pipeline, nullptr);
|
|
vkDestroyPipelineLayout(device->logicalDevice, pipelineLayout, nullptr);
|
|
vkDestroyDescriptorPool(device->logicalDevice, descriptorPool, nullptr);
|
|
vkDestroyDescriptorSetLayout(device->logicalDevice, descriptorSetLayout, nullptr);
|
|
}
|
|
|
|
// Initialize styles, keys, etc.
|
|
void init(float width, float height)
|
|
{
|
|
// Color scheme
|
|
vulkanStyle = ImGui::GetStyle();
|
|
vulkanStyle.Colors[ImGuiCol_TitleBg] = ImVec4(1.0f, 0.0f, 0.0f, 0.6f);
|
|
vulkanStyle.Colors[ImGuiCol_TitleBgActive] = ImVec4(1.0f, 0.0f, 0.0f, 0.8f);
|
|
vulkanStyle.Colors[ImGuiCol_MenuBarBg] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f);
|
|
vulkanStyle.Colors[ImGuiCol_Header] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f);
|
|
vulkanStyle.Colors[ImGuiCol_CheckMark] = ImVec4(0.0f, 1.0f, 0.0f, 1.0f);
|
|
|
|
setStyle(0);
|
|
|
|
// Dimensions
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.DisplaySize = ImVec2(width, height);
|
|
io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f);
|
|
#if defined(_WIN32)
|
|
// If we directly work with os specific key codes, we need to map special key types like tab
|
|
io.KeyMap[ImGuiKey_Tab] = VK_TAB;
|
|
io.KeyMap[ImGuiKey_LeftArrow] = VK_LEFT;
|
|
io.KeyMap[ImGuiKey_RightArrow] = VK_RIGHT;
|
|
io.KeyMap[ImGuiKey_UpArrow] = VK_UP;
|
|
io.KeyMap[ImGuiKey_DownArrow] = VK_DOWN;
|
|
io.KeyMap[ImGuiKey_Backspace] = VK_BACK;
|
|
io.KeyMap[ImGuiKey_Enter] = VK_RETURN;
|
|
io.KeyMap[ImGuiKey_Space] = VK_SPACE;
|
|
io.KeyMap[ImGuiKey_Delete] = VK_DELETE;
|
|
#endif
|
|
}
|
|
|
|
void setStyle(uint32_t index)
|
|
{
|
|
switch (index)
|
|
{
|
|
case 0:
|
|
{
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
style = vulkanStyle;
|
|
break;
|
|
}
|
|
case 1:
|
|
ImGui::StyleColorsClassic();
|
|
break;
|
|
case 2:
|
|
ImGui::StyleColorsDark();
|
|
break;
|
|
case 3:
|
|
ImGui::StyleColorsLight();
|
|
break;
|
|
case 4: // Blue theme
|
|
{
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
style = ImGui::GetStyle();
|
|
style.Colors[ImGuiCol_TitleBg] = ImVec4(0.0f, 0.3f, 0.8f, 0.6f);
|
|
style.Colors[ImGuiCol_TitleBgActive] = ImVec4(0.0f, 0.4f, 1.0f, 0.8f);
|
|
style.Colors[ImGuiCol_MenuBarBg] = ImVec4(0.0f, 0.2f, 0.6f, 0.4f);
|
|
style.Colors[ImGuiCol_Header] = ImVec4(0.0f, 0.3f, 0.7f, 0.4f);
|
|
style.Colors[ImGuiCol_CheckMark] = ImVec4(0.0f, 1.0f, 1.0f, 1.0f);
|
|
style.Colors[ImGuiCol_WindowBg] = ImVec4(0.05f, 0.05f, 0.15f, 0.9f);
|
|
break;
|
|
}
|
|
case 5: // Green theme
|
|
{
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
style = ImGui::GetStyle();
|
|
style.Colors[ImGuiCol_TitleBg] = ImVec4(0.0f, 0.6f, 0.2f, 0.6f);
|
|
style.Colors[ImGuiCol_TitleBgActive] = ImVec4(0.0f, 0.8f, 0.3f, 0.8f);
|
|
style.Colors[ImGuiCol_MenuBarBg] = ImVec4(0.0f, 0.4f, 0.1f, 0.4f);
|
|
style.Colors[ImGuiCol_Header] = ImVec4(0.0f, 0.5f, 0.2f, 0.4f);
|
|
style.Colors[ImGuiCol_CheckMark] = ImVec4(0.0f, 1.0f, 0.0f, 1.0f);
|
|
style.Colors[ImGuiCol_WindowBg] = ImVec4(0.05f, 0.15f, 0.05f, 0.9f);
|
|
break;
|
|
}
|
|
case 6: // Purple theme
|
|
{
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
style = ImGui::GetStyle();
|
|
style.Colors[ImGuiCol_TitleBg] = ImVec4(0.5f, 0.0f, 0.8f, 0.6f);
|
|
style.Colors[ImGuiCol_TitleBgActive] = ImVec4(0.7f, 0.0f, 1.0f, 0.8f);
|
|
style.Colors[ImGuiCol_MenuBarBg] = ImVec4(0.3f, 0.0f, 0.6f, 0.4f);
|
|
style.Colors[ImGuiCol_Header] = ImVec4(0.4f, 0.0f, 0.7f, 0.4f);
|
|
style.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f, 0.0f, 1.0f, 1.0f);
|
|
style.Colors[ImGuiCol_WindowBg] = ImVec4(0.1f, 0.05f, 0.15f, 0.9f);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize all Vulkan resources used by the ui
|
|
void initResources(VkRenderPass renderPass, VkQueue copyQueue, const std::string& shadersPath, VkFormat colorFormat = VK_FORMAT_UNDEFINED, VkFormat depthFormat = VK_FORMAT_UNDEFINED)
|
|
{
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
// Create font texture
|
|
unsigned char* fontData;
|
|
int texWidth, texHeight;
|
|
io.Fonts->GetTexDataAsRGBA32(&fontData, &texWidth, &texHeight);
|
|
VkDeviceSize uploadSize = texWidth*texHeight * 4 * sizeof(char);
|
|
|
|
//SRS - Get Vulkan device driver information if available, use later for display
|
|
if (device->extensionSupported(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME))
|
|
{
|
|
VkPhysicalDeviceProperties2 deviceProperties2 = {};
|
|
deviceProperties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
|
deviceProperties2.pNext = &driverProperties;
|
|
driverProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES;
|
|
vkGetPhysicalDeviceProperties2(device->physicalDevice, &deviceProperties2);
|
|
}
|
|
|
|
// Create target image for copy
|
|
VkImageCreateInfo imageInfo = vks::initializers::imageCreateInfo();
|
|
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
|
imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
|
|
imageInfo.extent.width = texWidth;
|
|
imageInfo.extent.height = texHeight;
|
|
imageInfo.extent.depth = 1;
|
|
imageInfo.mipLevels = 1;
|
|
imageInfo.arrayLayers = 1;
|
|
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
VK_CHECK_RESULT(vkCreateImage(device->logicalDevice, &imageInfo, nullptr, &fontImage));
|
|
VkMemoryRequirements memReqs;
|
|
vkGetImageMemoryRequirements(device->logicalDevice, fontImage, &memReqs);
|
|
VkMemoryAllocateInfo memAllocInfo = vks::initializers::memoryAllocateInfo();
|
|
memAllocInfo.allocationSize = memReqs.size;
|
|
memAllocInfo.memoryTypeIndex = device->getMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
VK_CHECK_RESULT(vkAllocateMemory(device->logicalDevice, &memAllocInfo, nullptr, &fontMemory));
|
|
VK_CHECK_RESULT(vkBindImageMemory(device->logicalDevice, fontImage, fontMemory, 0));
|
|
|
|
// Image view
|
|
VkImageViewCreateInfo viewInfo = vks::initializers::imageViewCreateInfo();
|
|
viewInfo.image = fontImage;
|
|
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
|
|
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
viewInfo.subresourceRange.levelCount = 1;
|
|
viewInfo.subresourceRange.layerCount = 1;
|
|
VK_CHECK_RESULT(vkCreateImageView(device->logicalDevice, &viewInfo, nullptr, &fontView));
|
|
|
|
// Staging buffers for font data upload
|
|
vks::Buffer stagingBuffer;
|
|
|
|
VK_CHECK_RESULT(device->createBuffer(
|
|
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
|
&stagingBuffer,
|
|
uploadSize));
|
|
|
|
stagingBuffer.map();
|
|
memcpy(stagingBuffer.mapped, fontData, uploadSize);
|
|
stagingBuffer.unmap();
|
|
|
|
// Copy buffer data to font image
|
|
VkCommandBuffer copyCmd = device->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
|
|
|
|
// Prepare for transfer
|
|
vks::tools::setImageLayout(
|
|
copyCmd,
|
|
fontImage,
|
|
VK_IMAGE_ASPECT_COLOR_BIT,
|
|
VK_IMAGE_LAYOUT_UNDEFINED,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_PIPELINE_STAGE_HOST_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
|
|
// Copy
|
|
VkBufferImageCopy bufferCopyRegion = {};
|
|
bufferCopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
bufferCopyRegion.imageSubresource.layerCount = 1;
|
|
bufferCopyRegion.imageExtent.width = texWidth;
|
|
bufferCopyRegion.imageExtent.height = texHeight;
|
|
bufferCopyRegion.imageExtent.depth = 1;
|
|
|
|
vkCmdCopyBufferToImage(
|
|
copyCmd,
|
|
stagingBuffer.buffer,
|
|
fontImage,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
1,
|
|
&bufferCopyRegion
|
|
);
|
|
|
|
// Prepare for shader read
|
|
vks::tools::setImageLayout(
|
|
copyCmd,
|
|
fontImage,
|
|
VK_IMAGE_ASPECT_COLOR_BIT,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
|
|
|
device->flushCommandBuffer(copyCmd, copyQueue, true);
|
|
|
|
stagingBuffer.destroy();
|
|
|
|
// Font texture Sampler
|
|
VkSamplerCreateInfo samplerInfo = vks::initializers::samplerCreateInfo();
|
|
samplerInfo.magFilter = VK_FILTER_LINEAR;
|
|
samplerInfo.minFilter = VK_FILTER_LINEAR;
|
|
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
|
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
samplerInfo.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
|
VK_CHECK_RESULT(vkCreateSampler(device->logicalDevice, &samplerInfo, nullptr, &sampler));
|
|
|
|
// Descriptor pool
|
|
std::vector<VkDescriptorPoolSize> poolSizes = {
|
|
vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1)
|
|
};
|
|
VkDescriptorPoolCreateInfo descriptorPoolInfo = vks::initializers::descriptorPoolCreateInfo(poolSizes, 2);
|
|
VK_CHECK_RESULT(vkCreateDescriptorPool(device->logicalDevice, &descriptorPoolInfo, nullptr, &descriptorPool));
|
|
|
|
// Descriptor set layout
|
|
std::vector<VkDescriptorSetLayoutBinding> setLayoutBindings = {
|
|
vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 0),
|
|
};
|
|
VkDescriptorSetLayoutCreateInfo descriptorLayout = vks::initializers::descriptorSetLayoutCreateInfo(setLayoutBindings);
|
|
VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device->logicalDevice, &descriptorLayout, nullptr, &descriptorSetLayout));
|
|
|
|
// Descriptor set
|
|
VkDescriptorSetAllocateInfo allocInfo = vks::initializers::descriptorSetAllocateInfo(descriptorPool, &descriptorSetLayout, 1);
|
|
VK_CHECK_RESULT(vkAllocateDescriptorSets(device->logicalDevice, &allocInfo, &descriptorSet));
|
|
VkDescriptorImageInfo fontDescriptor = vks::initializers::descriptorImageInfo(
|
|
sampler,
|
|
fontView,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
|
|
);
|
|
std::vector<VkWriteDescriptorSet> writeDescriptorSets = {
|
|
vks::initializers::writeDescriptorSet(descriptorSet, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 0, &fontDescriptor)
|
|
};
|
|
vkUpdateDescriptorSets(device->logicalDevice, static_cast<uint32_t>(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, nullptr);
|
|
|
|
// Pipeline cache
|
|
VkPipelineCacheCreateInfo pipelineCacheCreateInfo = {};
|
|
pipelineCacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
|
|
VK_CHECK_RESULT(vkCreatePipelineCache(device->logicalDevice, &pipelineCacheCreateInfo, nullptr, &pipelineCache));
|
|
|
|
// Pipeline layout
|
|
// Push constants for UI rendering parameters
|
|
VkPushConstantRange pushConstantRange = vks::initializers::pushConstantRange(VK_SHADER_STAGE_VERTEX_BIT, sizeof(PushConstBlock), 0);
|
|
VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = vks::initializers::pipelineLayoutCreateInfo(&descriptorSetLayout, 1);
|
|
pipelineLayoutCreateInfo.pushConstantRangeCount = 1;
|
|
pipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange;
|
|
VK_CHECK_RESULT(vkCreatePipelineLayout(device->logicalDevice, &pipelineLayoutCreateInfo, nullptr, &pipelineLayout));
|
|
|
|
// Setup graphics pipeline for UI rendering
|
|
VkPipelineInputAssemblyStateCreateInfo inputAssemblyState =
|
|
vks::initializers::pipelineInputAssemblyStateCreateInfo(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE);
|
|
|
|
VkPipelineRasterizationStateCreateInfo rasterizationState =
|
|
vks::initializers::pipelineRasterizationStateCreateInfo(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE);
|
|
|
|
// Enable blending
|
|
VkPipelineColorBlendAttachmentState blendAttachmentState{};
|
|
blendAttachmentState.blendEnable = VK_TRUE;
|
|
blendAttachmentState.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
|
blendAttachmentState.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
|
|
blendAttachmentState.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
|
blendAttachmentState.colorBlendOp = VK_BLEND_OP_ADD;
|
|
blendAttachmentState.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
|
blendAttachmentState.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
|
|
blendAttachmentState.alphaBlendOp = VK_BLEND_OP_ADD;
|
|
|
|
VkPipelineColorBlendStateCreateInfo colorBlendState =
|
|
vks::initializers::pipelineColorBlendStateCreateInfo(1, &blendAttachmentState);
|
|
|
|
VkPipelineDepthStencilStateCreateInfo depthStencilState =
|
|
vks::initializers::pipelineDepthStencilStateCreateInfo(VK_FALSE, VK_FALSE, VK_COMPARE_OP_LESS_OR_EQUAL);
|
|
|
|
VkPipelineViewportStateCreateInfo viewportState =
|
|
vks::initializers::pipelineViewportStateCreateInfo(1, 1, 0);
|
|
|
|
VkPipelineMultisampleStateCreateInfo multisampleState =
|
|
vks::initializers::pipelineMultisampleStateCreateInfo(VK_SAMPLE_COUNT_1_BIT);
|
|
|
|
std::vector<VkDynamicState> dynamicStateEnables = {
|
|
VK_DYNAMIC_STATE_VIEWPORT,
|
|
VK_DYNAMIC_STATE_SCISSOR
|
|
};
|
|
VkPipelineDynamicStateCreateInfo dynamicState =
|
|
vks::initializers::pipelineDynamicStateCreateInfo(dynamicStateEnables);
|
|
|
|
std::array<VkPipelineShaderStageCreateInfo, 2> shaderStages{};
|
|
|
|
// Create pipeline - use dynamic rendering if no render pass provided
|
|
VkGraphicsPipelineCreateInfo pipelineCreateInfo;
|
|
if (renderPass == VK_NULL_HANDLE) {
|
|
// Dynamic rendering
|
|
pipelineCreateInfo = vks::initializers::pipelineCreateInfo();
|
|
pipelineCreateInfo.layout = pipelineLayout;
|
|
} else {
|
|
// Traditional render pass
|
|
pipelineCreateInfo = vks::initializers::pipelineCreateInfo(pipelineLayout, renderPass);
|
|
}
|
|
|
|
pipelineCreateInfo.pInputAssemblyState = &inputAssemblyState;
|
|
pipelineCreateInfo.pRasterizationState = &rasterizationState;
|
|
pipelineCreateInfo.pColorBlendState = &colorBlendState;
|
|
pipelineCreateInfo.pMultisampleState = &multisampleState;
|
|
pipelineCreateInfo.pViewportState = &viewportState;
|
|
pipelineCreateInfo.pDepthStencilState = &depthStencilState;
|
|
pipelineCreateInfo.pDynamicState = &dynamicState;
|
|
pipelineCreateInfo.stageCount = static_cast<uint32_t>(shaderStages.size());
|
|
pipelineCreateInfo.pStages = shaderStages.data();
|
|
|
|
// Vertex bindings an attributes based on ImGui vertex definition
|
|
std::vector<VkVertexInputBindingDescription> vertexInputBindings = {
|
|
vks::initializers::vertexInputBindingDescription(0, sizeof(ImDrawVert), VK_VERTEX_INPUT_RATE_VERTEX),
|
|
};
|
|
std::vector<VkVertexInputAttributeDescription> vertexInputAttributes = {
|
|
vks::initializers::vertexInputAttributeDescription(0, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(ImDrawVert, pos)), // Location 0: Position
|
|
vks::initializers::vertexInputAttributeDescription(0, 1, VK_FORMAT_R32G32_SFLOAT, offsetof(ImDrawVert, uv)), // Location 1: UV
|
|
vks::initializers::vertexInputAttributeDescription(0, 2, VK_FORMAT_R8G8B8A8_UNORM, offsetof(ImDrawVert, col)), // Location 0: Color
|
|
};
|
|
VkPipelineVertexInputStateCreateInfo vertexInputState = vks::initializers::pipelineVertexInputStateCreateInfo();
|
|
vertexInputState.vertexBindingDescriptionCount = static_cast<uint32_t>(vertexInputBindings.size());
|
|
vertexInputState.pVertexBindingDescriptions = vertexInputBindings.data();
|
|
vertexInputState.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertexInputAttributes.size());
|
|
vertexInputState.pVertexAttributeDescriptions = vertexInputAttributes.data();
|
|
|
|
pipelineCreateInfo.pVertexInputState = &vertexInputState;
|
|
|
|
// Dynamic rendering create info for ImGui pipeline (only if using dynamic rendering)
|
|
VkPipelineRenderingCreateInfoKHR pipelineRenderingCreateInfo{};
|
|
if (renderPass == VK_NULL_HANDLE && colorFormat != VK_FORMAT_UNDEFINED) {
|
|
pipelineRenderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
|
|
pipelineRenderingCreateInfo.colorAttachmentCount = 1;
|
|
pipelineRenderingCreateInfo.pColorAttachmentFormats = &colorFormat;
|
|
pipelineRenderingCreateInfo.depthAttachmentFormat = depthFormat;
|
|
pipelineRenderingCreateInfo.stencilAttachmentFormat = depthFormat;
|
|
// Chain into the pipeline create info
|
|
pipelineCreateInfo.pNext = &pipelineRenderingCreateInfo;
|
|
}
|
|
|
|
shaderStages[0] = example->loadShader(shadersPath + "imgui/ui.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
|
|
shaderStages[1] = example->loadShader(shadersPath + "imgui/ui.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
|
|
|
|
VK_CHECK_RESULT(vkCreateGraphicsPipelines(device->logicalDevice, pipelineCache, 1, &pipelineCreateInfo, nullptr, &pipeline));
|
|
}
|
|
|
|
|
|
// Starts a new imGui frame and sets up windows and ui elements
|
|
void newFrame(VulkanExampleBase *example, bool updateFrameGraph)
|
|
{
|
|
ImGui::NewFrame();
|
|
|
|
// Menu Bar
|
|
if (ImGui::BeginMainMenuBar())
|
|
{
|
|
if (ImGui::BeginMenu("File"))
|
|
{
|
|
if (ImGui::MenuItem("New Scene", "Ctrl+N")) {
|
|
// Clear scene
|
|
sceneManager.clearScene();
|
|
}
|
|
if (ImGui::MenuItem("Open Scene", "Ctrl+O")) {
|
|
// TODO: Implement scene loading
|
|
}
|
|
if (ImGui::MenuItem("Save Scene", "Ctrl+S")) {
|
|
// TODO: Implement scene saving
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Exit", "Alt+F4")) {
|
|
example->prepared = false;
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
if (ImGui::BeginMenu("Preferences"))
|
|
{
|
|
if (ImGui::BeginMenu("UI Theme"))
|
|
{
|
|
if (ImGui::MenuItem("Vulkan Red", nullptr, selectedStyle == 0)) { setStyle(0); selectedStyle = 0; }
|
|
if (ImGui::MenuItem("Classic", nullptr, selectedStyle == 1)) { setStyle(1); selectedStyle = 1; }
|
|
if (ImGui::MenuItem("Dark", nullptr, selectedStyle == 2)) { setStyle(2); selectedStyle = 2; }
|
|
if (ImGui::MenuItem("Light", nullptr, selectedStyle == 3)) { setStyle(3); selectedStyle = 3; }
|
|
if (ImGui::MenuItem("Blue", nullptr, selectedStyle == 4)) { setStyle(4); selectedStyle = 4; }
|
|
if (ImGui::MenuItem("Green", nullptr, selectedStyle == 5)) { setStyle(5); selectedStyle = 5; }
|
|
if (ImGui::MenuItem("Purple", nullptr, selectedStyle == 6)) { setStyle(6); selectedStyle = 6; }
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::Separator();
|
|
// Viewport settings moved to dedicated Viewport panel
|
|
ImGui::EndMenu();
|
|
}
|
|
if (ImGui::BeginMenu("Windows"))
|
|
{
|
|
ImGui::MenuItem("Scene Hierarchy", nullptr, &uiSettings.showSceneHierarchyPanel);
|
|
ImGui::MenuItem("Inspector", nullptr, &uiSettings.showInspectorPanel);
|
|
ImGui::MenuItem("Viewport", nullptr, &uiSettings.showViewportPanel);
|
|
ImGui::MenuItem("Asset Browser", nullptr, &uiSettings.showAssetBrowserPanel);
|
|
ImGui::MenuItem("Console", nullptr, &uiSettings.showConsolePanel);
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Show All Panels")) {
|
|
uiSettings.showSceneHierarchyPanel = true;
|
|
uiSettings.showInspectorPanel = true;
|
|
uiSettings.showViewportPanel = true;
|
|
uiSettings.showAssetBrowserPanel = true;
|
|
uiSettings.showConsolePanel = true;
|
|
}
|
|
if (ImGui::MenuItem("Hide All Panels")) {
|
|
uiSettings.showSceneHierarchyPanel = false;
|
|
uiSettings.showInspectorPanel = false;
|
|
uiSettings.showViewportPanel = false;
|
|
uiSettings.showAssetBrowserPanel = false;
|
|
uiSettings.showConsolePanel = false;
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
if (ImGui::BeginMenu("Help"))
|
|
{
|
|
if (ImGui::MenuItem("About")) {
|
|
// TODO: Show about dialog
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::EndMainMenuBar();
|
|
}
|
|
|
|
// Init imGui windows and elements
|
|
|
|
// Debug window
|
|
ImGui::SetWindowPos(ImVec2(20 * example->ui.scale, 20 * example->ui.scale), ImGuiSetCond_FirstUseEver);
|
|
ImGui::SetWindowSize(ImVec2(300 * example->ui.scale, 300 * example->ui.scale), ImGuiSetCond_Always);
|
|
ImGui::TextUnformatted(example->title.c_str());
|
|
ImGui::TextUnformatted(device->properties.deviceName);
|
|
|
|
//SRS - Display Vulkan API version and device driver information if available (otherwise blank)
|
|
ImGui::Text("Vulkan API %i.%i.%i", VK_API_VERSION_MAJOR(device->properties.apiVersion), VK_API_VERSION_MINOR(device->properties.apiVersion), VK_API_VERSION_PATCH(device->properties.apiVersion));
|
|
ImGui::Text("%s %s", driverProperties.driverName, driverProperties.driverInfo);
|
|
|
|
// Update frame time display
|
|
if (updateFrameGraph) {
|
|
std::rotate(uiSettings.frameTimes.begin(), uiSettings.frameTimes.begin() + 1, uiSettings.frameTimes.end());
|
|
float frameTime = 1000.0f / (example->frameTimer * 1000.0f);
|
|
uiSettings.frameTimes.back() = frameTime;
|
|
if (frameTime < uiSettings.frameTimeMin) {
|
|
uiSettings.frameTimeMin = frameTime;
|
|
}
|
|
if (frameTime > uiSettings.frameTimeMax) {
|
|
uiSettings.frameTimeMax = frameTime;
|
|
}
|
|
}
|
|
|
|
ImGui::PlotLines("Frame Times", &uiSettings.frameTimes[0], 50, 0, "", uiSettings.frameTimeMin, uiSettings.frameTimeMax, ImVec2(0, 80));
|
|
|
|
ImGui::Text("Orbit Camera");
|
|
ImGui::Text("Enhanced camera system active");
|
|
ImGui::Separator();
|
|
ImGui::Text("Controls:");
|
|
ImGui::BulletText("F - Focus on selection");
|
|
ImGui::BulletText("Alt+LMB - Orbit");
|
|
ImGui::BulletText("Alt+MMB - Pan");
|
|
ImGui::BulletText("Alt+RMB - Zoom");
|
|
ImGui::BulletText("Mouse Wheel - Zoom");
|
|
|
|
|
|
// Simple Scene Hierarchy Panel
|
|
if (uiSettings.showSceneHierarchyPanel) {
|
|
ImGui::SetNextWindowPos(ImVec2(20 * example->ui.scale, 360 * example->ui.scale), ImGuiSetCond_FirstUseEver);
|
|
ImGui::SetNextWindowSize(ImVec2(300 * example->ui.scale, 400 * example->ui.scale), ImGuiSetCond_FirstUseEver);
|
|
ImGui::Begin("Scene Hierarchy", &uiSettings.showSceneHierarchyPanel);
|
|
|
|
// Header with object count
|
|
ImGui::Text("Scene Objects: %d", sceneManager.getObjectCount());
|
|
ImGui::Separator();
|
|
|
|
// Render simple object list
|
|
for (int i = 0; i < sceneManager.objects.size(); i++) {
|
|
const auto& obj = sceneManager.objects[i];
|
|
bool isSelected = (sceneManager.selectedIndex == i);
|
|
|
|
// Object icon based on type
|
|
const char* icon = "[P]"; // Default procedural icon
|
|
if (obj.type == "Model") icon = "[M]";
|
|
|
|
// Visibility toggle (use object name as unique ImGui ID)
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
|
ImGui::PushID(obj.name.c_str()); // Use object name for unique ImGui ID
|
|
if (ImGui::SmallButton(obj.visible ? "V" : "H")) {
|
|
// Toggle visibility (note: const_cast needed for modification)
|
|
const_cast<SceneObject&>(obj).visible = !obj.visible;
|
|
}
|
|
ImGui::PopID();
|
|
ImGui::PopStyleColor();
|
|
ImGui::SameLine();
|
|
|
|
// Selectable object name with highlighting for selected objects
|
|
if (isSelected) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); // Gold color for selected
|
|
}
|
|
if (ImGui::Selectable((std::string(icon) + " " + obj.name).c_str(), isSelected)) {
|
|
sceneManager.setSelectedIndex(i);
|
|
std::cout << "Selected object: " << obj.name << std::endl;
|
|
}
|
|
if (isSelected) {
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Right-click context menu
|
|
if (ImGui::BeginPopupContextItem()) {
|
|
if (ImGui::MenuItem("Delete", "Del")) {
|
|
sceneManager.removeObject(i);
|
|
ImGui::EndPopup();
|
|
break; // Exit loop since we modified the vector
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
// Show message if scene is empty
|
|
if (sceneManager.getObjectCount() == 0) {
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Scene is empty");
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Add objects from Asset Browser");
|
|
}
|
|
|
|
ImGui::End();
|
|
} // End Scene Hierarchy Panel visibility check
|
|
|
|
// Inspector Panel
|
|
if (uiSettings.showInspectorPanel) {
|
|
ImGui::SetNextWindowPos(ImVec2(1180 * example->ui.scale, 20 * example->ui.scale), ImGuiSetCond_FirstUseEver);
|
|
ImGui::SetNextWindowSize(ImVec2(300 * example->ui.scale, 700 * example->ui.scale), ImGuiSetCond_FirstUseEver);
|
|
ImGui::Begin("Inspector", &uiSettings.showInspectorPanel);
|
|
ImGui::Text("Object Properties:");
|
|
ImGui::Separator();
|
|
|
|
if (sceneManager.selectedIndex >= 0 && sceneManager.selectedIndex < sceneManager.objects.size()) {
|
|
auto& selectedObj = sceneManager.objects[sceneManager.selectedIndex]; // Non-const for editing
|
|
|
|
// Object Header with Name and Type
|
|
ImGui::Text("Selected: %s", selectedObj.name.c_str());
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "(%s)", selectedObj.type.c_str());
|
|
ImGui::Separator();
|
|
|
|
// Transform Section (collapsible like legacy 3D engine)
|
|
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
// Position Vector3 control
|
|
float pos[3] = { selectedObj.position.x, selectedObj.position.y, selectedObj.position.z };
|
|
if (ImGui::DragFloat3("Position", pos, 0.1f)) {
|
|
selectedObj.position = glm::vec3(pos[0], pos[1], pos[2]);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Reset##pos")) {
|
|
selectedObj.position = glm::vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
// Rotation Vector3 control (degrees)
|
|
float rot[3] = { selectedObj.rotation.x, selectedObj.rotation.y, selectedObj.rotation.z };
|
|
if (ImGui::DragFloat3("Rotation", rot, 1.0f, -360.0f, 360.0f)) {
|
|
selectedObj.rotation = glm::vec3(rot[0], rot[1], rot[2]);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Reset##rot")) {
|
|
selectedObj.rotation = glm::vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
// Scale Vector3 control
|
|
float scale[3] = { selectedObj.scale.x, selectedObj.scale.y, selectedObj.scale.z };
|
|
if (ImGui::DragFloat3("Scale", scale, 0.01f, 0.01f, 10.0f)) {
|
|
selectedObj.scale = glm::vec3(scale[0], scale[1], scale[2]);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Reset##scale")) {
|
|
selectedObj.scale = glm::vec3(1.0f, 1.0f, 1.0f); // Default scale is 1.0
|
|
}
|
|
|
|
// Transform utilities
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Reset All Transform")) {
|
|
selectedObj.position = glm::vec3(0.0f, 0.0f, 0.0f);
|
|
selectedObj.rotation = glm::vec3(0.0f, 0.0f, 0.0f);
|
|
selectedObj.scale = glm::vec3(1.0f, 1.0f, 1.0f);
|
|
}
|
|
}
|
|
|
|
// Object Properties Section
|
|
if (ImGui::CollapsingHeader("Object Properties", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
// Visibility toggle
|
|
bool visible = selectedObj.visible;
|
|
if (ImGui::Checkbox("Visible", &visible)) {
|
|
selectedObj.visible = visible;
|
|
}
|
|
|
|
// Object name editing
|
|
static char nameBuffer[256];
|
|
strncpy_s(nameBuffer, selectedObj.name.c_str(), sizeof(nameBuffer) - 1);
|
|
if (ImGui::InputText("Name", nameBuffer, sizeof(nameBuffer))) {
|
|
selectedObj.name = std::string(nameBuffer);
|
|
}
|
|
}
|
|
|
|
// Procedural Parameters Section (for procedural shapes)
|
|
if (selectedObj.type == "Procedural") {
|
|
if (ImGui::CollapsingHeader("Procedural Parameters", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
ImGui::Text("Shape Type: %s", selectedObj.subtype.c_str());
|
|
|
|
// Dynamic parameter controls based on shape type
|
|
bool parametersChanged = false;
|
|
|
|
if (selectedObj.subtype == "Cube") {
|
|
ImGui::Text("Cube Parameters:");
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::DragFloat("Width", &selectedObj.proceduralParams.cubeWidth, 0.1f, 0.1f, 10.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::DragFloat("Height", &selectedObj.proceduralParams.cubeHeight, 0.1f, 0.1f, 10.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::DragFloat("Depth", &selectedObj.proceduralParams.cubeDepth, 0.1f, 0.1f, 10.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::SliderInt("Subdivisions", &selectedObj.proceduralParams.cubeSubdivisions, 1, 10)) {
|
|
parametersChanged = true;
|
|
}
|
|
|
|
if (ImGui::Button("Reset Cube Parameters")) {
|
|
selectedObj.proceduralParams.cubeWidth = 2.0f;
|
|
selectedObj.proceduralParams.cubeHeight = 2.0f;
|
|
selectedObj.proceduralParams.cubeDepth = 2.0f;
|
|
selectedObj.proceduralParams.cubeSubdivisions = 1;
|
|
parametersChanged = true;
|
|
}
|
|
|
|
} else if (selectedObj.subtype == "Sphere") {
|
|
ImGui::Text("Sphere Parameters:");
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::DragFloat("Radius", &selectedObj.proceduralParams.sphereRadius, 0.05f, 0.1f, 5.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::SliderInt("Segments", &selectedObj.proceduralParams.sphereSegments, 4, 64)) {
|
|
parametersChanged = true;
|
|
}
|
|
|
|
if (ImGui::Button("Reset Sphere Parameters")) {
|
|
selectedObj.proceduralParams.sphereRadius = 1.0f;
|
|
selectedObj.proceduralParams.sphereSegments = 16;
|
|
parametersChanged = true;
|
|
}
|
|
|
|
} else if (selectedObj.subtype == "Cylinder") {
|
|
ImGui::Text("Cylinder Parameters:");
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::DragFloat("Radius", &selectedObj.proceduralParams.cylinderRadius, 0.05f, 0.1f, 5.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::DragFloat("Height", &selectedObj.proceduralParams.cylinderHeight, 0.1f, 0.1f, 10.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::SliderInt("Segments", &selectedObj.proceduralParams.cylinderSegments, 3, 64)) {
|
|
parametersChanged = true;
|
|
}
|
|
|
|
if (ImGui::Button("Reset Cylinder Parameters")) {
|
|
selectedObj.proceduralParams.cylinderRadius = 1.0f;
|
|
selectedObj.proceduralParams.cylinderHeight = 2.0f;
|
|
selectedObj.proceduralParams.cylinderSegments = 16;
|
|
parametersChanged = true;
|
|
}
|
|
|
|
} else if (selectedObj.subtype == "Cone") {
|
|
ImGui::Text("Cone Parameters:");
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::DragFloat("Radius", &selectedObj.proceduralParams.coneRadius, 0.05f, 0.1f, 5.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::DragFloat("Height", &selectedObj.proceduralParams.coneHeight, 0.1f, 0.1f, 10.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::SliderInt("Segments", &selectedObj.proceduralParams.coneSegments, 3, 64)) {
|
|
parametersChanged = true;
|
|
}
|
|
|
|
if (ImGui::Button("Reset Cone Parameters")) {
|
|
selectedObj.proceduralParams.coneRadius = 1.0f;
|
|
selectedObj.proceduralParams.coneHeight = 2.0f;
|
|
selectedObj.proceduralParams.coneSegments = 16;
|
|
parametersChanged = true;
|
|
}
|
|
|
|
} else if (selectedObj.subtype == "Plane") {
|
|
ImGui::Text("Plane Parameters:");
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::DragFloat("Width", &selectedObj.proceduralParams.planeWidth, 0.1f, 0.1f, 10.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::DragFloat("Height", &selectedObj.proceduralParams.planeHeight, 0.1f, 0.1f, 10.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::SliderInt("Subdivisions", &selectedObj.proceduralParams.planeSubdivisions, 1, 20)) {
|
|
parametersChanged = true;
|
|
}
|
|
|
|
if (ImGui::Button("Reset Plane Parameters")) {
|
|
selectedObj.proceduralParams.planeWidth = 2.0f;
|
|
selectedObj.proceduralParams.planeHeight = 2.0f;
|
|
selectedObj.proceduralParams.planeSubdivisions = 1;
|
|
parametersChanged = true;
|
|
}
|
|
|
|
} else if (selectedObj.subtype == "Torus") {
|
|
ImGui::Text("Torus Parameters:");
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::DragFloat("Major Radius", &selectedObj.proceduralParams.torusMajorRadius, 0.05f, 0.2f, 5.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::DragFloat("Minor Radius", &selectedObj.proceduralParams.torusMinorRadius, 0.02f, 0.05f, 2.0f, "%.2f")) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::SliderInt("Major Segments", &selectedObj.proceduralParams.torusMajorSegments, 3, 64)) {
|
|
parametersChanged = true;
|
|
}
|
|
if (ImGui::SliderInt("Minor Segments", &selectedObj.proceduralParams.torusMinorSegments, 3, 32)) {
|
|
parametersChanged = true;
|
|
}
|
|
|
|
if (ImGui::Button("Reset Torus Parameters")) {
|
|
selectedObj.proceduralParams.torusMajorRadius = 1.0f;
|
|
selectedObj.proceduralParams.torusMinorRadius = 0.3f;
|
|
selectedObj.proceduralParams.torusMajorSegments = 16;
|
|
selectedObj.proceduralParams.torusMinorSegments = 8;
|
|
parametersChanged = true;
|
|
}
|
|
}
|
|
|
|
// Regenerate geometry if parameters changed
|
|
if (parametersChanged) {
|
|
selectedObj.regenerateGeometry();
|
|
std::cout << "Regenerated geometry for " << selectedObj.name << " (" << selectedObj.subtype << ")" << std::endl;
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "✓ Real-time parameter editing enabled");
|
|
}
|
|
}
|
|
} else {
|
|
ImGui::Text("Selected: None");
|
|
ImGui::Text("Select an object in the Scene Hierarchy");
|
|
ImGui::Separator();
|
|
}
|
|
|
|
|
|
|
|
// UI Style selection is available in Preferences > UI Theme menu
|
|
|
|
ImGui::End();
|
|
} // End Inspector Panel visibility check
|
|
|
|
// Viewport Panel - Settings for 3D viewport rendering
|
|
if (uiSettings.showViewportPanel) {
|
|
ImGui::SetNextWindowPos(ImVec2(660 * example->ui.scale, 350 * example->ui.scale), ImGuiSetCond_FirstUseEver);
|
|
ImGui::SetNextWindowSize(ImVec2(320 * example->ui.scale, 280 * example->ui.scale), ImGuiSetCond_FirstUseEver);
|
|
ImGui::Begin("Viewport", &uiSettings.showViewportPanel);
|
|
|
|
ImGui::Text("3D Viewport Settings:");
|
|
ImGui::Separator();
|
|
|
|
// Render Mode Settings
|
|
if (ImGui::CollapsingHeader("Render Mode", ImGuiTreeNodeFlags_DefaultOpen))
|
|
{
|
|
// TODO: Add render mode dropdown (Solid, X-Ray, etc.) when renderer supports it
|
|
ImGui::Text("Mode: Solid"); // Placeholder until render modes implemented
|
|
}
|
|
|
|
// Grid Settings (moved from Inspector)
|
|
if (ImGui::CollapsingHeader("Grid Settings", ImGuiTreeNodeFlags_DefaultOpen))
|
|
{
|
|
ImGui::Checkbox("Show Grid", &uiSettings.showGrid);
|
|
if (uiSettings.showGrid) {
|
|
ImGui::DragFloat("Grid Size", &uiSettings.gridSize, 0.5f, 1.0f, 50.0f);
|
|
ImGui::DragInt("Grid Divisions", &uiSettings.gridDivisions, 1, 2, 50);
|
|
|
|
// Grid color picker (placeholder - needs renderer integration)
|
|
static float gridColor[3] = { 0.5f, 0.5f, 0.5f };
|
|
ImGui::ColorEdit3("Grid Color", gridColor);
|
|
}
|
|
|
|
// Note about grid rendering implementation
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.3f, 1.0f), "Note: Grid rendering requires Vulkan renderer integration.");
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Settings are saved but grid is not yet rendered in viewport.");
|
|
}
|
|
|
|
// Camera Settings
|
|
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen))
|
|
{
|
|
ImGui::Text("Current Camera: Orbit Camera");
|
|
ImGui::Text("Controls:");
|
|
ImGui::BulletText("Alt + LMB: Orbit");
|
|
ImGui::BulletText("Alt + MMB: Pan");
|
|
ImGui::BulletText("Alt + RMB: Zoom");
|
|
ImGui::BulletText("Mouse Wheel: Zoom");
|
|
ImGui::BulletText("F Key: Focus Selection");
|
|
}
|
|
|
|
// Selection Tools
|
|
if (ImGui::CollapsingHeader("Selection Tools"))
|
|
{
|
|
ImGui::Text("Active Tool: Rectangle Select");
|
|
// TODO: Add gizmo tool selection when gizmos implemented
|
|
}
|
|
|
|
// Lighting Settings (moved from Inspector panel)
|
|
if (ImGui::CollapsingHeader("Lighting", ImGuiTreeNodeFlags_DefaultOpen))
|
|
{
|
|
ImGui::Checkbox("Animate light", &uiSettings.animateLight);
|
|
ImGui::SliderFloat("Light speed", &uiSettings.lightSpeed, 0.1f, 1.0f);
|
|
}
|
|
|
|
ImGui::End();
|
|
} // End Viewport Panel visibility check
|
|
|
|
// Asset Browser Panel
|
|
if (uiSettings.showAssetBrowserPanel) {
|
|
ImGui::SetNextWindowPos(ImVec2(20 * example->ui.scale, 780 * example->ui.scale), ImGuiSetCond_FirstUseEver);
|
|
ImGui::SetNextWindowSize(ImVec2(600 * example->ui.scale, 220 * example->ui.scale), ImGuiSetCond_FirstUseEver);
|
|
ImGui::Begin("Asset Browser", &uiSettings.showAssetBrowserPanel);
|
|
ImGui::Text("Project Assets:");
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::CollapsingHeader("Procedural Shapes", ImGuiTreeNodeFlags_DefaultOpen))
|
|
{
|
|
ImGui::Text("Basic Geometric Shapes:");
|
|
ImGui::Separator();
|
|
|
|
// Create buttons for each shape type - First row
|
|
if (ImGui::Button("Cube")) {
|
|
sceneManager.addProceduralShape("Cube", "Cube");
|
|
}
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add a procedural cube to the scene");
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Sphere")) {
|
|
sceneManager.addProceduralShape("Sphere", "Sphere");
|
|
}
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add a procedural sphere to the scene");
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Plane")) {
|
|
sceneManager.addProceduralShape("Plane", "Plane");
|
|
}
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add a procedural plane to the scene");
|
|
|
|
// Second row
|
|
if (ImGui::Button("Cone")) {
|
|
sceneManager.addProceduralShape("Cone", "Cone");
|
|
}
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add a procedural cone to the scene");
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cylinder")) {
|
|
sceneManager.addProceduralShape("Cylinder", "Cylinder");
|
|
}
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add a procedural cylinder to the scene");
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Torus")) {
|
|
sceneManager.addProceduralShape("Torus", "Torus");
|
|
}
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add a procedural torus to the scene");
|
|
}
|
|
|
|
if (ImGui::CollapsingHeader("Models"))
|
|
{
|
|
ImGui::Text("• MobulaBirostris.gltf");
|
|
ImGui::Text("• PolarBear.gltf");
|
|
}
|
|
if (ImGui::CollapsingHeader("Textures"))
|
|
{
|
|
ImGui::Text("• Loading textures from glTF files...");
|
|
}
|
|
if (ImGui::CollapsingHeader("Materials"))
|
|
{
|
|
ImGui::Text("• Default Vulkan Materials");
|
|
}
|
|
ImGui::End();
|
|
} // End Asset Browser Panel visibility check
|
|
|
|
// Console Panel
|
|
if (uiSettings.showConsolePanel) {
|
|
ImGui::SetNextWindowPos(ImVec2(640 * example->ui.scale, 780 * example->ui.scale), ImGuiSetCond_FirstUseEver);
|
|
ImGui::SetNextWindowSize(ImVec2(840 * example->ui.scale, 200 * example->ui.scale), ImGuiSetCond_FirstUseEver);
|
|
ImGui::Begin("Console", &uiSettings.showConsolePanel);
|
|
ImGui::Text("System Console:");
|
|
ImGui::Separator();
|
|
ImGui::Text("[INFO] ProceduralEngine - Vulkan Renderer initialized");
|
|
ImGui::Text("[INFO] Scene loaded successfully - Ready for procedural generation");
|
|
ImGui::Separator();
|
|
static char inputBuf[256] = "";
|
|
if (ImGui::InputText("Command", inputBuf, sizeof(inputBuf), ImGuiInputTextFlags_EnterReturnsTrue))
|
|
{
|
|
// Process command
|
|
inputBuf[0] = '\0';
|
|
}
|
|
ImGui::End();
|
|
} // End Console Panel visibility check
|
|
|
|
//SRS - ShowDemoWindow() sets its own initial position and size, cannot override here
|
|
// ImGui::ShowDemoWindow();
|
|
|
|
// Render to generate draw buffers
|
|
ImGui::Render();
|
|
}
|
|
|
|
// Update vertex and index buffer containing the imGui elements when required
|
|
void updateBuffers()
|
|
{
|
|
ImDrawData* imDrawData = ImGui::GetDrawData();
|
|
|
|
// Note: Alignment is done inside buffer creation
|
|
VkDeviceSize vertexBufferSize = imDrawData->TotalVtxCount * sizeof(ImDrawVert);
|
|
VkDeviceSize indexBufferSize = imDrawData->TotalIdxCount * sizeof(ImDrawIdx);
|
|
|
|
if ((vertexBufferSize == 0) || (indexBufferSize == 0)) {
|
|
return;
|
|
}
|
|
|
|
// Update buffers only if vertex or index count has been changed compared to current buffer size
|
|
|
|
// Vertex buffer
|
|
if ((vertexBuffer.buffer == VK_NULL_HANDLE) || (vertexCount != imDrawData->TotalVtxCount)) {
|
|
vertexBuffer.unmap();
|
|
vertexBuffer.destroy();
|
|
VK_CHECK_RESULT(device->createBuffer(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, &vertexBuffer, vertexBufferSize));
|
|
vertexCount = imDrawData->TotalVtxCount;
|
|
vertexBuffer.map();
|
|
}
|
|
|
|
// Index buffer
|
|
if ((indexBuffer.buffer == VK_NULL_HANDLE) || (indexCount < imDrawData->TotalIdxCount)) {
|
|
indexBuffer.unmap();
|
|
indexBuffer.destroy();
|
|
VK_CHECK_RESULT(device->createBuffer(VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, &indexBuffer, indexBufferSize));
|
|
indexCount = imDrawData->TotalIdxCount;
|
|
indexBuffer.map();
|
|
}
|
|
|
|
// Upload data
|
|
ImDrawVert* vtxDst = (ImDrawVert*)vertexBuffer.mapped;
|
|
ImDrawIdx* idxDst = (ImDrawIdx*)indexBuffer.mapped;
|
|
|
|
for (int n = 0; n < imDrawData->CmdListsCount; n++) {
|
|
const ImDrawList* cmd_list = imDrawData->CmdLists[n];
|
|
memcpy(vtxDst, cmd_list->VtxBuffer.Data, cmd_list->VtxBuffer.Size * sizeof(ImDrawVert));
|
|
memcpy(idxDst, cmd_list->IdxBuffer.Data, cmd_list->IdxBuffer.Size * sizeof(ImDrawIdx));
|
|
vtxDst += cmd_list->VtxBuffer.Size;
|
|
idxDst += cmd_list->IdxBuffer.Size;
|
|
}
|
|
|
|
// Flush to make writes visible to GPU
|
|
vertexBuffer.flush();
|
|
indexBuffer.flush();
|
|
}
|
|
|
|
// Draw current imGui frame into a command buffer
|
|
void drawFrame(VkCommandBuffer commandBuffer)
|
|
{
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
|
|
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
|
|
|
VkViewport viewport = vks::initializers::viewport(ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y, 0.0f, 1.0f);
|
|
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
|
|
|
|
// UI scale and translate via push constants
|
|
pushConstBlock.scale = glm::vec2(2.0f / io.DisplaySize.x, 2.0f / io.DisplaySize.y);
|
|
pushConstBlock.translate = glm::vec2(-1.0f);
|
|
vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(PushConstBlock), &pushConstBlock);
|
|
|
|
// Render commands
|
|
ImDrawData* imDrawData = ImGui::GetDrawData();
|
|
int32_t vertexOffset = 0;
|
|
int32_t indexOffset = 0;
|
|
|
|
if (imDrawData->CmdListsCount > 0) {
|
|
|
|
VkDeviceSize offsets[1] = { 0 };
|
|
vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vertexBuffer.buffer, offsets);
|
|
vkCmdBindIndexBuffer(commandBuffer, indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT16);
|
|
|
|
for (int32_t i = 0; i < imDrawData->CmdListsCount; i++)
|
|
{
|
|
const ImDrawList* cmd_list = imDrawData->CmdLists[i];
|
|
for (int32_t j = 0; j < cmd_list->CmdBuffer.Size; j++)
|
|
{
|
|
const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[j];
|
|
VkRect2D scissorRect;
|
|
scissorRect.offset.x = std::max((int32_t)(pcmd->ClipRect.x), 0);
|
|
scissorRect.offset.y = std::max((int32_t)(pcmd->ClipRect.y), 0);
|
|
scissorRect.extent.width = (uint32_t)(pcmd->ClipRect.z - pcmd->ClipRect.x);
|
|
scissorRect.extent.height = (uint32_t)(pcmd->ClipRect.w - pcmd->ClipRect.y);
|
|
vkCmdSetScissor(commandBuffer, 0, 1, &scissorRect);
|
|
vkCmdDrawIndexed(commandBuffer, pcmd->ElemCount, 1, indexOffset, vertexOffset, 0);
|
|
indexOffset += pcmd->ElemCount;
|
|
}
|
|
#if (defined(VK_USE_PLATFORM_IOS_MVK) || defined(VK_USE_PLATFORM_METAL_EXT)) && TARGET_OS_SIMULATOR
|
|
// Apple Device Simulator does not support vkCmdDrawIndexed() with vertexOffset > 0, so rebind vertex buffer instead
|
|
offsets[0] += cmd_list->VtxBuffer.Size * sizeof(ImDrawVert);
|
|
vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vertexBuffer.buffer, offsets);
|
|
#else
|
|
vertexOffset += cmd_list->VtxBuffer.Size;
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// VulkanExample
|
|
// ----------------------------------------------------------------------------
|
|
|
|
class VulkanExample : public VulkanExampleBase
|
|
{
|
|
public:
|
|
// Dynamic rendering function pointers
|
|
PFN_vkCmdBeginRenderingKHR vkCmdBeginRenderingKHR{ VK_NULL_HANDLE };
|
|
PFN_vkCmdEndRenderingKHR vkCmdEndRenderingKHR{ VK_NULL_HANDLE };
|
|
|
|
VkPhysicalDeviceDynamicRenderingFeaturesKHR enabledDynamicRenderingFeaturesKHR{};
|
|
|
|
ImGUI *imGui = nullptr;
|
|
|
|
struct Models {
|
|
vkglTF::Model models;
|
|
vkglTF::Model logos;
|
|
vkglTF::Model background;
|
|
} models;
|
|
|
|
|
|
vks::Buffer uniformBufferVS;
|
|
|
|
struct UBOVS {
|
|
glm::mat4 projection;
|
|
glm::mat4 model;
|
|
glm::vec4 lightPos;
|
|
} uboVS;
|
|
|
|
VkPipelineLayout pipelineLayout;
|
|
VkPipeline pipeline;
|
|
VkDescriptorSetLayout descriptorSetLayout;
|
|
VkDescriptorSet descriptorSet;
|
|
|
|
// Procedural shapes pipeline
|
|
VkPipelineLayout proceduralPipelineLayout;
|
|
VkPipeline proceduralPipeline;
|
|
VkDescriptorSetLayout proceduralDescriptorSetLayout;
|
|
VkDescriptorSet proceduralDescriptorSet;
|
|
|
|
// Enhanced orbit camera
|
|
OrbitCamera orbitCamera;
|
|
|
|
// Mouse interaction state
|
|
bool mouseInteracting = false;
|
|
double lastMouseX = 0.0;
|
|
double lastMouseY = 0.0;
|
|
|
|
VulkanExample() : VulkanExampleBase(), orbitCamera(glm::vec3(5.0f, 3.0f, 5.0f), glm::vec3(0.0f, 0.0f, -5.0f))
|
|
{
|
|
title = "ProceduralEngine - Vulkan 3D Viewport";
|
|
camera.type = Camera::CameraType::lookat;
|
|
camera.setPosition(glm::vec3(0.0f, 0.0f, -8.0f));
|
|
camera.setRotation(glm::vec3(4.5f, -380.0f, 0.0f));
|
|
camera.setPerspective(45.0f, (float)width / (float)height, 0.1f, 256.0f);
|
|
|
|
// Configure orbit camera with legacy-compatible settings
|
|
orbitCamera.SetSensitivity(0.5f, 0.003f, 0.1f); // Match legacy sensitivity values
|
|
orbitCamera.SetSmoothingFactor(0.15f); // Match legacy smoothing
|
|
|
|
//SRS - Enable VK_KHR_get_physical_device_properties2 to retrieve device driver information for display
|
|
enabledInstanceExtensions.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
|
|
|
|
// Enable dynamic rendering extensions
|
|
enabledDeviceExtensions.push_back(VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME);
|
|
enabledDeviceExtensions.push_back(VK_KHR_MAINTENANCE2_EXTENSION_NAME);
|
|
enabledDeviceExtensions.push_back(VK_KHR_MULTIVIEW_EXTENSION_NAME);
|
|
enabledDeviceExtensions.push_back(VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME);
|
|
enabledDeviceExtensions.push_back(VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME);
|
|
|
|
// Enable dynamic rendering features
|
|
enabledDynamicRenderingFeaturesKHR.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR;
|
|
enabledDynamicRenderingFeaturesKHR.dynamicRendering = VK_TRUE;
|
|
|
|
deviceCreatepNextChain = &enabledDynamicRenderingFeaturesKHR;
|
|
|
|
// Don't use the ImGui overlay of the base framework in this sample
|
|
settings.overlay = false;
|
|
}
|
|
|
|
~VulkanExample()
|
|
{
|
|
vkDestroyPipeline(device, pipeline, nullptr);
|
|
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
|
|
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
|
|
|
|
vkDestroyPipeline(device, proceduralPipeline, nullptr);
|
|
vkDestroyPipelineLayout(device, proceduralPipelineLayout, nullptr);
|
|
vkDestroyDescriptorSetLayout(device, proceduralDescriptorSetLayout, nullptr);
|
|
|
|
uniformBufferVS.destroy();
|
|
|
|
delete imGui;
|
|
}
|
|
|
|
void setupRenderPass()
|
|
{
|
|
// With VK_KHR_dynamic_rendering we no longer need a render pass, so skip the sample base render pass setup
|
|
renderPass = VK_NULL_HANDLE;
|
|
}
|
|
|
|
void setupFrameBuffer()
|
|
{
|
|
// With VK_KHR_dynamic_rendering we no longer need a frame buffer, so skip the sample base framebuffer setup
|
|
}
|
|
|
|
void renderProceduralShapes(VkCommandBuffer commandBuffer) {
|
|
// Render procedural shapes using persistent buffers
|
|
static int frameCount = 0;
|
|
frameCount++;
|
|
|
|
for (auto& obj : sceneManager.objects) {
|
|
if (obj.type == "Procedural" && obj.visible && !obj.indices.empty() && obj.buffersCreated) {
|
|
// Calculate model matrix for this object
|
|
glm::mat4 model = glm::mat4(1.0f);
|
|
model = glm::translate(model, obj.position);
|
|
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
|
|
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
|
|
model = glm::scale(model, obj.scale);
|
|
|
|
// Update uniform buffer with object's model matrix while preserving camera matrices
|
|
UBOVS tempUBO;
|
|
tempUBO.projection = orbitCamera.GetProjectionMatrix((float)width / (float)height, 0.1f, 256.0f); // Use OrbitCamera projection
|
|
tempUBO.model = orbitCamera.GetViewMatrix() * model; // Combine view and model matrices
|
|
tempUBO.lightPos = uboVS.lightPos; // Preserve light position
|
|
|
|
VK_CHECK_RESULT(uniformBufferVS.map());
|
|
memcpy(uniformBufferVS.mapped, &tempUBO, sizeof(tempUBO));
|
|
uniformBufferVS.unmap();
|
|
|
|
// Validate buffers before binding
|
|
if (obj.vertexBuffer.buffer == VK_NULL_HANDLE || obj.indexBuffer.buffer == VK_NULL_HANDLE) {
|
|
std::cout << "Warning: Invalid buffer handles for " << obj.name << std::endl;
|
|
continue;
|
|
}
|
|
|
|
// Bind the vertex and index buffers for this object
|
|
VkBuffer vertexBuffers[] = { obj.vertexBuffer.buffer };
|
|
VkDeviceSize offsets[] = { 0 };
|
|
vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
|
|
vkCmdBindIndexBuffer(commandBuffer, obj.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);
|
|
|
|
// Draw the object
|
|
vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(obj.indices.size()), 1, 0, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void buildCommandBuffers()
|
|
{
|
|
VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo();
|
|
|
|
imGui->newFrame(this, (frameCounter == 0));
|
|
imGui->updateBuffers();
|
|
|
|
for (int32_t i = 0; i < drawCmdBuffers.size(); ++i)
|
|
{
|
|
VK_CHECK_RESULT(vkBeginCommandBuffer(drawCmdBuffers[i], &cmdBufInfo));
|
|
|
|
// Dynamic rendering requires manual image layout transitions
|
|
// Prepare color attachment for rendering
|
|
vks::tools::insertImageMemoryBarrier(
|
|
drawCmdBuffers[i],
|
|
swapChain.images[i],
|
|
0,
|
|
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
|
VK_IMAGE_LAYOUT_UNDEFINED,
|
|
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
|
VkImageSubresourceRange{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 });
|
|
|
|
// Prepare depth attachment for rendering
|
|
vks::tools::insertImageMemoryBarrier(
|
|
drawCmdBuffers[i],
|
|
depthStencil.image,
|
|
0,
|
|
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
|
VK_IMAGE_LAYOUT_UNDEFINED,
|
|
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
|
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
|
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
|
VkImageSubresourceRange{ VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT, 0, 1, 0, 1 });
|
|
|
|
// Set up rendering attachments for dynamic rendering
|
|
VkRenderingAttachmentInfoKHR colorAttachment{};
|
|
colorAttachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR;
|
|
colorAttachment.imageView = swapChain.imageViews[i];
|
|
colorAttachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
|
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
colorAttachment.clearValue.color = { 0.2f, 0.2f, 0.2f, 1.0f };
|
|
|
|
VkRenderingAttachmentInfoKHR depthStencilAttachment{};
|
|
depthStencilAttachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR;
|
|
depthStencilAttachment.imageView = depthStencil.view;
|
|
depthStencilAttachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
depthStencilAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
|
depthStencilAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
depthStencilAttachment.clearValue.depthStencil = { 1.0f, 0 };
|
|
|
|
VkRenderingInfoKHR renderingInfo{};
|
|
renderingInfo.sType = VK_STRUCTURE_TYPE_RENDERING_INFO_KHR;
|
|
renderingInfo.renderArea = { 0, 0, width, height };
|
|
renderingInfo.layerCount = 1;
|
|
renderingInfo.colorAttachmentCount = 1;
|
|
renderingInfo.pColorAttachments = &colorAttachment;
|
|
renderingInfo.pDepthAttachment = &depthStencilAttachment;
|
|
renderingInfo.pStencilAttachment = &depthStencilAttachment;
|
|
|
|
// Begin dynamic rendering
|
|
vkCmdBeginRenderingKHR(drawCmdBuffers[i], &renderingInfo);
|
|
|
|
VkViewport viewport = vks::initializers::viewport((float)width, (float)height, 0.0f, 1.0f);
|
|
vkCmdSetViewport(drawCmdBuffers[i], 0, 1, &viewport);
|
|
|
|
VkRect2D scissor = vks::initializers::rect2D(width, height, 0, 0);
|
|
vkCmdSetScissor(drawCmdBuffers[i], 0, 1, &scissor);
|
|
|
|
// Render vkglTF models with original pipeline
|
|
vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
|
|
vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
|
|
|
if (uiSettings.displayBackground) {
|
|
models.background.draw(drawCmdBuffers[i]);
|
|
}
|
|
|
|
if (uiSettings.displayModels) {
|
|
models.models.draw(drawCmdBuffers[i]);
|
|
}
|
|
|
|
// Switch to procedural pipeline for procedural shapes
|
|
vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, proceduralPipelineLayout, 0, 1, &proceduralDescriptorSet, 0, nullptr);
|
|
vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, proceduralPipeline);
|
|
|
|
// Render procedural shapes
|
|
renderProceduralShapes(drawCmdBuffers[i]);
|
|
|
|
// Render imGui
|
|
if (ui.visible) {
|
|
imGui->drawFrame(drawCmdBuffers[i]);
|
|
}
|
|
|
|
// End dynamic rendering
|
|
vkCmdEndRenderingKHR(drawCmdBuffers[i]);
|
|
|
|
// Prepare image for presentation
|
|
vks::tools::insertImageMemoryBarrier(
|
|
drawCmdBuffers[i],
|
|
swapChain.images[i],
|
|
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
|
0,
|
|
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
|
|
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
|
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
|
VkImageSubresourceRange{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 });
|
|
|
|
VK_CHECK_RESULT(vkEndCommandBuffer(drawCmdBuffers[i]));
|
|
}
|
|
}
|
|
|
|
void setupLayoutsAndDescriptors()
|
|
{
|
|
// descriptor pool (increased to handle procedural pipeline too)
|
|
std::vector<VkDescriptorPoolSize> poolSizes = {
|
|
vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4),
|
|
vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1)
|
|
};
|
|
VkDescriptorPoolCreateInfo descriptorPoolInfo = vks::initializers::descriptorPoolCreateInfo(poolSizes, 4);
|
|
VK_CHECK_RESULT(vkCreateDescriptorPool(device, &descriptorPoolInfo, nullptr, &descriptorPool));
|
|
|
|
// Set layout
|
|
std::vector<VkDescriptorSetLayoutBinding> setLayoutBindings = {
|
|
vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 0),
|
|
};
|
|
VkDescriptorSetLayoutCreateInfo descriptorLayout =
|
|
vks::initializers::descriptorSetLayoutCreateInfo(setLayoutBindings);
|
|
VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorLayout, nullptr, &descriptorSetLayout));
|
|
|
|
// Pipeline layout
|
|
VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = vks::initializers::pipelineLayoutCreateInfo(&descriptorSetLayout, 1);
|
|
VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutCreateInfo, nullptr, &pipelineLayout));
|
|
|
|
// Descriptor set
|
|
VkDescriptorSetAllocateInfo allocInfo = vks::initializers::descriptorSetAllocateInfo(descriptorPool, &descriptorSetLayout, 1);
|
|
VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet));
|
|
std::vector<VkWriteDescriptorSet> writeDescriptorSets = {
|
|
vks::initializers::writeDescriptorSet(descriptorSet, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &uniformBufferVS.descriptor),
|
|
};
|
|
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, nullptr);
|
|
|
|
// Allocate procedural descriptor set (note: proceduralDescriptorSetLayout is created in prepareProceduralPipeline)
|
|
// This will be done after pipelines are prepared
|
|
}
|
|
|
|
void setupProceduralDescriptorSet()
|
|
{
|
|
// Allocate procedural descriptor set
|
|
VkDescriptorSetAllocateInfo allocInfo = vks::initializers::descriptorSetAllocateInfo(descriptorPool, &proceduralDescriptorSetLayout, 1);
|
|
VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &proceduralDescriptorSet));
|
|
std::vector<VkWriteDescriptorSet> writeDescriptorSets = {
|
|
vks::initializers::writeDescriptorSet(proceduralDescriptorSet, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &uniformBufferVS.descriptor),
|
|
};
|
|
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, nullptr);
|
|
}
|
|
|
|
void preparePipelines()
|
|
{
|
|
// Rendering
|
|
VkPipelineInputAssemblyStateCreateInfo inputAssemblyState = vks::initializers::pipelineInputAssemblyStateCreateInfo(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE);
|
|
VkPipelineRasterizationStateCreateInfo rasterizationState = vks::initializers::pipelineRasterizationStateCreateInfo(VK_POLYGON_MODE_FILL, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_COUNTER_CLOCKWISE);
|
|
VkPipelineColorBlendAttachmentState blendAttachmentState = vks::initializers::pipelineColorBlendAttachmentState(0xf, VK_FALSE);
|
|
VkPipelineColorBlendStateCreateInfo colorBlendState = vks::initializers::pipelineColorBlendStateCreateInfo(1, &blendAttachmentState);
|
|
VkPipelineDepthStencilStateCreateInfo depthStencilState = vks::initializers::pipelineDepthStencilStateCreateInfo(VK_TRUE, VK_TRUE, VK_COMPARE_OP_LESS_OR_EQUAL);
|
|
VkPipelineViewportStateCreateInfo viewportState = vks::initializers::pipelineViewportStateCreateInfo(1, 1, 0);
|
|
VkPipelineMultisampleStateCreateInfo multisampleState = vks::initializers::pipelineMultisampleStateCreateInfo(VK_SAMPLE_COUNT_1_BIT);
|
|
std::vector<VkDynamicState> dynamicStateEnables = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
|
|
VkPipelineDynamicStateCreateInfo dynamicState = vks::initializers::pipelineDynamicStateCreateInfo(dynamicStateEnables);
|
|
|
|
std::array<VkPipelineShaderStageCreateInfo,2> shaderStages;
|
|
|
|
// Create pipeline without render pass for dynamic rendering
|
|
VkGraphicsPipelineCreateInfo pipelineCI = vks::initializers::pipelineCreateInfo();
|
|
pipelineCI.layout = pipelineLayout;
|
|
pipelineCI.pInputAssemblyState = &inputAssemblyState;
|
|
pipelineCI.pRasterizationState = &rasterizationState;
|
|
pipelineCI.pColorBlendState = &colorBlendState;
|
|
pipelineCI.pMultisampleState = &multisampleState;
|
|
pipelineCI.pViewportState = &viewportState;
|
|
pipelineCI.pDepthStencilState = &depthStencilState;
|
|
pipelineCI.pDynamicState = &dynamicState;
|
|
pipelineCI.stageCount = static_cast<uint32_t>(shaderStages.size());
|
|
pipelineCI.pStages = shaderStages.data();
|
|
pipelineCI.pVertexInputState = vkglTF::Vertex::getPipelineVertexInputState({ vkglTF::VertexComponent::Position, vkglTF::VertexComponent::Normal, vkglTF::VertexComponent::Color });;
|
|
|
|
// Dynamic rendering create info to define color, depth and stencil attachments at pipeline create time
|
|
VkPipelineRenderingCreateInfoKHR pipelineRenderingCreateInfo{};
|
|
pipelineRenderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
|
|
pipelineRenderingCreateInfo.colorAttachmentCount = 1;
|
|
pipelineRenderingCreateInfo.pColorAttachmentFormats = &swapChain.colorFormat;
|
|
pipelineRenderingCreateInfo.depthAttachmentFormat = depthFormat;
|
|
pipelineRenderingCreateInfo.stencilAttachmentFormat = depthFormat;
|
|
// Chain into the pipeline create info
|
|
pipelineCI.pNext = &pipelineRenderingCreateInfo;
|
|
|
|
shaderStages[0] = loadShader(getShadersPath() + "imgui/scene.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
|
|
shaderStages[1] = loadShader(getShadersPath() + "imgui/scene.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
|
|
VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCI, nullptr, &pipeline));
|
|
|
|
// Create procedural shapes pipeline
|
|
prepareProceduralPipeline();
|
|
}
|
|
|
|
void prepareProceduralPipeline()
|
|
{
|
|
// Descriptor set layout for procedural shapes (uniform buffer only)
|
|
VkDescriptorSetLayoutBinding layoutBinding = vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 0);
|
|
VkDescriptorSetLayoutCreateInfo descriptorLayout = vks::initializers::descriptorSetLayoutCreateInfo(&layoutBinding, 1);
|
|
VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorLayout, nullptr, &proceduralDescriptorSetLayout));
|
|
|
|
// Pipeline layout
|
|
VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = vks::initializers::pipelineLayoutCreateInfo(&proceduralDescriptorSetLayout, 1);
|
|
VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutCreateInfo, nullptr, &proceduralPipelineLayout));
|
|
|
|
// Pipeline
|
|
VkPipelineInputAssemblyStateCreateInfo inputAssemblyState = vks::initializers::pipelineInputAssemblyStateCreateInfo(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE);
|
|
VkPipelineRasterizationStateCreateInfo rasterizationState = vks::initializers::pipelineRasterizationStateCreateInfo(VK_POLYGON_MODE_FILL, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_COUNTER_CLOCKWISE);
|
|
VkPipelineColorBlendAttachmentState blendAttachmentState = vks::initializers::pipelineColorBlendAttachmentState(0xf, VK_FALSE);
|
|
VkPipelineColorBlendStateCreateInfo colorBlendState = vks::initializers::pipelineColorBlendStateCreateInfo(1, &blendAttachmentState);
|
|
VkPipelineDepthStencilStateCreateInfo depthStencilState = vks::initializers::pipelineDepthStencilStateCreateInfo(VK_TRUE, VK_TRUE, VK_COMPARE_OP_LESS_OR_EQUAL);
|
|
VkPipelineViewportStateCreateInfo viewportState = vks::initializers::pipelineViewportStateCreateInfo(1, 1, 0);
|
|
VkPipelineMultisampleStateCreateInfo multisampleState = vks::initializers::pipelineMultisampleStateCreateInfo(VK_SAMPLE_COUNT_1_BIT);
|
|
std::vector<VkDynamicState> dynamicStateEnables = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
|
|
VkPipelineDynamicStateCreateInfo dynamicState = vks::initializers::pipelineDynamicStateCreateInfo(dynamicStateEnables);
|
|
|
|
// Vertex input for procedural shapes
|
|
VkVertexInputBindingDescription vertexInputBinding = vks::initializers::vertexInputBindingDescription(0, sizeof(ProceduralVertex), VK_VERTEX_INPUT_RATE_VERTEX);
|
|
std::vector<VkVertexInputAttributeDescription> vertexInputAttributes = {
|
|
vks::initializers::vertexInputAttributeDescription(0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0), // Position
|
|
vks::initializers::vertexInputAttributeDescription(0, 1, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3), // Normal
|
|
vks::initializers::vertexInputAttributeDescription(0, 2, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 6), // Color
|
|
};
|
|
VkPipelineVertexInputStateCreateInfo vertexInputState = vks::initializers::pipelineVertexInputStateCreateInfo();
|
|
vertexInputState.vertexBindingDescriptionCount = 1;
|
|
vertexInputState.pVertexBindingDescriptions = &vertexInputBinding;
|
|
vertexInputState.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertexInputAttributes.size());
|
|
vertexInputState.pVertexAttributeDescriptions = vertexInputAttributes.data();
|
|
|
|
std::array<VkPipelineShaderStageCreateInfo, 2> shaderStages;
|
|
|
|
VkGraphicsPipelineCreateInfo pipelineCI = vks::initializers::pipelineCreateInfo();
|
|
pipelineCI.layout = proceduralPipelineLayout;
|
|
pipelineCI.pInputAssemblyState = &inputAssemblyState;
|
|
pipelineCI.pRasterizationState = &rasterizationState;
|
|
pipelineCI.pColorBlendState = &colorBlendState;
|
|
pipelineCI.pMultisampleState = &multisampleState;
|
|
pipelineCI.pViewportState = &viewportState;
|
|
pipelineCI.pDepthStencilState = &depthStencilState;
|
|
pipelineCI.pDynamicState = &dynamicState;
|
|
pipelineCI.pVertexInputState = &vertexInputState;
|
|
pipelineCI.stageCount = static_cast<uint32_t>(shaderStages.size());
|
|
pipelineCI.pStages = shaderStages.data();
|
|
|
|
// Dynamic rendering create info
|
|
VkPipelineRenderingCreateInfoKHR pipelineRenderingCreateInfo{};
|
|
pipelineRenderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
|
|
pipelineRenderingCreateInfo.colorAttachmentCount = 1;
|
|
pipelineRenderingCreateInfo.pColorAttachmentFormats = &swapChain.colorFormat;
|
|
pipelineRenderingCreateInfo.depthAttachmentFormat = depthFormat;
|
|
pipelineRenderingCreateInfo.stencilAttachmentFormat = depthFormat;
|
|
pipelineCI.pNext = &pipelineRenderingCreateInfo;
|
|
|
|
shaderStages[0] = loadShader(getShadersPath() + "imgui/scene.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
|
|
shaderStages[1] = loadShader(getShadersPath() + "imgui/scene.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
|
|
VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCI, nullptr, &proceduralPipeline));
|
|
}
|
|
|
|
// Prepare and initialize uniform buffer containing shader uniforms
|
|
void prepareUniformBuffers()
|
|
{
|
|
// Vertex shader uniform buffer block
|
|
VK_CHECK_RESULT(vulkanDevice->createBuffer(
|
|
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
|
&uniformBufferVS,
|
|
sizeof(uboVS),
|
|
&uboVS));
|
|
|
|
updateUniformBuffers();
|
|
}
|
|
|
|
void updateUniformBuffers()
|
|
{
|
|
// Update orbit camera
|
|
orbitCamera.Update(frameTimer);
|
|
|
|
// Vertex shader - use OrbitCamera matrices
|
|
uboVS.projection = orbitCamera.GetProjectionMatrix((float)width / (float)height, 0.1f, 256.0f);
|
|
uboVS.model = orbitCamera.GetViewMatrix() * glm::mat4(1.0f);
|
|
|
|
// Light source
|
|
if (uiSettings.animateLight) {
|
|
uiSettings.lightTimer += frameTimer * uiSettings.lightSpeed;
|
|
uboVS.lightPos.x = sin(glm::radians(uiSettings.lightTimer * 360.0f)) * 15.0f;
|
|
uboVS.lightPos.z = cos(glm::radians(uiSettings.lightTimer * 360.0f)) * 15.0f;
|
|
};
|
|
|
|
VK_CHECK_RESULT(uniformBufferVS.map());
|
|
memcpy(uniformBufferVS.mapped, &uboVS, sizeof(uboVS));
|
|
uniformBufferVS.unmap();
|
|
}
|
|
|
|
void draw()
|
|
{
|
|
VulkanExampleBase::prepareFrame();
|
|
|
|
// Check if we need to create buffers for new objects
|
|
bool needsCommandBufferRebuild = false;
|
|
for (auto& obj : sceneManager.objects) {
|
|
if (obj.type == "Procedural" && !obj.buffersCreated && !obj.vertices.empty() && !obj.indices.empty()) {
|
|
// Wait for GPU to finish current operations before creating new buffers
|
|
vkDeviceWaitIdle(device);
|
|
sceneManager.createBuffersForObject(obj);
|
|
needsCommandBufferRebuild = true;
|
|
}
|
|
}
|
|
|
|
// If new objects were added, ensure proper synchronization
|
|
if (needsCommandBufferRebuild) {
|
|
vkDeviceWaitIdle(device);
|
|
}
|
|
|
|
buildCommandBuffers();
|
|
submitInfo.commandBufferCount = 1;
|
|
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
|
|
VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE));
|
|
VulkanExampleBase::submitFrame();
|
|
}
|
|
|
|
void loadAssets()
|
|
{
|
|
const uint32_t glTFLoadingFlags = vkglTF::FileLoadingFlags::PreTransformVertices | vkglTF::FileLoadingFlags::PreMultiplyVertexColors | vkglTF::FileLoadingFlags::FlipY;
|
|
// Models available in assets but not auto-loaded
|
|
// models.models.loadFromFile(getAssetPath() + "models/MobulaBirostris.gltf", vulkanDevice, queue, glTFLoadingFlags);
|
|
// models.background.loadFromFile(getAssetPath() + "models/PolarBear.gltf", vulkanDevice, queue, glTFLoadingFlags);
|
|
}
|
|
|
|
void prepareImGui()
|
|
{
|
|
imGui = new ImGUI(this);
|
|
imGui->init((float)width, (float)height);
|
|
imGui->initResources(renderPass, queue, getShadersPath(), swapChain.colorFormat, depthFormat);
|
|
}
|
|
|
|
void prepare()
|
|
{
|
|
VulkanExampleBase::prepare();
|
|
|
|
// Get dynamic rendering function pointers
|
|
vkCmdBeginRenderingKHR = reinterpret_cast<PFN_vkCmdBeginRenderingKHR>(vkGetDeviceProcAddr(device, "vkCmdBeginRenderingKHR"));
|
|
vkCmdEndRenderingKHR = reinterpret_cast<PFN_vkCmdEndRenderingKHR>(vkGetDeviceProcAddr(device, "vkCmdEndRenderingKHR"));
|
|
|
|
// Validate function pointers
|
|
if (!vkCmdBeginRenderingKHR || !vkCmdEndRenderingKHR) {
|
|
std::cout << "ERROR: Failed to load dynamic rendering function pointers" << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
// Initialize scene manager with device pointer
|
|
sceneManager.device = vulkanDevice;
|
|
|
|
loadAssets();
|
|
prepareUniformBuffers();
|
|
setupLayoutsAndDescriptors();
|
|
preparePipelines();
|
|
setupProceduralDescriptorSet();
|
|
prepareImGui();
|
|
buildCommandBuffers();
|
|
prepared = true;
|
|
}
|
|
|
|
virtual void render()
|
|
{
|
|
if (!prepared)
|
|
return;
|
|
|
|
updateUniformBuffers();
|
|
|
|
// Update imGui
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
io.DisplaySize = ImVec2((float)width, (float)height);
|
|
io.DeltaTime = frameTimer;
|
|
|
|
io.MousePos = ImVec2(mouseState.position.x, mouseState.position.y);
|
|
io.MouseDown[0] = mouseState.buttons.left && ui.visible;
|
|
io.MouseDown[1] = mouseState.buttons.right && ui.visible;
|
|
io.MouseDown[2] = mouseState.buttons.middle && ui.visible;
|
|
|
|
// Handle mouse wheel for camera zoom
|
|
if (io.MouseWheel != 0.0f) {
|
|
orbitCamera.ZoomImmediate(-io.MouseWheel * 10.0f, frameTimer);
|
|
}
|
|
|
|
draw();
|
|
}
|
|
|
|
|
|
// Input handling is platform specific, to show how it's basically done this sample implements it for Windows
|
|
#if defined(_WIN32)
|
|
virtual void OnHandleMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
// Handle mouse wheel for ImGui (needed for camera zoom)
|
|
if (uMsg == WM_MOUSEWHEEL) {
|
|
io.MouseWheel += (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
|
|
}
|
|
|
|
// Only react to keyboard input if ImGui is active
|
|
if (io.WantCaptureKeyboard) {
|
|
// Character input
|
|
if (uMsg == WM_CHAR) {
|
|
if (wParam > 0 && wParam < 0x10000) {
|
|
io.AddInputCharacter((unsigned short)wParam);
|
|
}
|
|
}
|
|
// Special keys (tab, cursor, etc.)
|
|
if ((wParam < 256) && (uMsg == WM_KEYDOWN || uMsg == WM_SYSKEYDOWN)) {
|
|
io.KeysDown[wParam] = true;
|
|
}
|
|
if ((wParam < 256) && (uMsg == WM_KEYUP || uMsg == WM_SYSKEYUP)) {
|
|
io.KeysDown[wParam] = false;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Override keyPressed for F key focus functionality
|
|
virtual void keyPressed(uint32_t keyCode) override
|
|
{
|
|
// Handle F key for focus (immediate, no smoothing)
|
|
if (keyCode == KEY_F) {
|
|
if (sceneManager.hasSelection()) {
|
|
// Focus on selected object immediately
|
|
SceneObject* selectedObject = sceneManager.getSelectedObject();
|
|
if (selectedObject) {
|
|
glm::vec3 objectCenter = selectedObject->getBoundingBoxCenter();
|
|
float objectRadius = selectedObject->getBoundingRadius();
|
|
orbitCamera.SetFocusToSelectionImmediate(objectCenter, objectRadius);
|
|
std::cout << "Focused camera on selected object: " << selectedObject->name << std::endl;
|
|
}
|
|
} else {
|
|
// No selection, focus on scene center immediately
|
|
orbitCamera.FrameAllImmediate(glm::vec3(0.0f, 0.0f, -5.0f), 3.0f);
|
|
std::cout << "Focused camera on scene center (no selection)" << std::endl;
|
|
}
|
|
}
|
|
|
|
|
|
// Call base implementation for other keys
|
|
VulkanExampleBase::keyPressed(keyCode);
|
|
}
|
|
|
|
// Override mouseMoved for camera orbit/pan controls
|
|
virtual void mouseMoved(double x, double y, bool &handled) override
|
|
{
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
// Calculate mouse delta
|
|
double deltaX = x - lastMouseX;
|
|
double deltaY = y - lastMouseY;
|
|
lastMouseX = x;
|
|
lastMouseY = y;
|
|
|
|
// Handle industry-standard Maya/3ds Max style camera controls (Alt + mouse)
|
|
// Use Windows API directly for Alt key detection since ImGui's KeyAlt is not reliable in this Vulkan framework
|
|
#if defined(_WIN32)
|
|
bool altPressed = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
|
|
#else
|
|
bool altPressed = io.KeyAlt; // Fallback for non-Windows platforms
|
|
#endif
|
|
|
|
// Alt key detection is now working properly using Windows GetAsyncKeyState API
|
|
|
|
if (altPressed && mouseState.buttons.left) {
|
|
// Alt + Left mouse: orbit around focus point
|
|
if (std::abs(deltaX) > 0.1 || std::abs(deltaY) > 0.1) {
|
|
orbitCamera.Orbit(deltaX * 0.5f, deltaY * 0.5f, frameTimer);
|
|
handled = true;
|
|
}
|
|
} else if (altPressed && mouseState.buttons.middle) {
|
|
// Alt + Middle mouse: pan viewport (immediate, no smoothing)
|
|
if (std::abs(deltaX) > 0.1 || std::abs(deltaY) > 0.1) {
|
|
orbitCamera.PanImmediate(-deltaX, deltaY, frameTimer);
|
|
handled = true;
|
|
}
|
|
} else if (altPressed && mouseState.buttons.right) {
|
|
// Alt + Right mouse: zoom (dolly camera)
|
|
if (std::abs(deltaY) > 0.1) {
|
|
orbitCamera.Zoom(deltaY * 0.01f, frameTimer);
|
|
handled = true;
|
|
}
|
|
}
|
|
|
|
// Call base implementation if not handled
|
|
if (!handled) {
|
|
VulkanExampleBase::mouseMoved(x, y, handled);
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
VULKAN_EXAMPLE_MAIN()
|