Yeah! PhysX!!!

This commit is contained in:
Anemunt
2025-12-16 12:02:05 -05:00
parent 978033c84d
commit 195eb73a73
12 changed files with 1336 additions and 34 deletions

View File

@@ -127,6 +127,5 @@ void ViewportController::update(GLFWwindow* window, bool& cursorLocked) {
viewportFocused = false;
manualUnfocus = true;
cursorLocked = false;
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
}
}

View File

@@ -273,17 +273,6 @@ void Engine::run() {
continue;
}
// Enforce cursor lock state every frame to avoid backends restoring it.
int desiredMode = cursorLocked ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL;
if (glfwGetInputMode(editorWindow, GLFW_CURSOR) != desiredMode) {
glfwSetInputMode(editorWindow, GLFW_CURSOR, desiredMode);
if (cursorLocked && glfwRawMouseMotionSupported()) {
glfwSetInputMode(editorWindow, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
} else if (!cursorLocked && glfwRawMouseMotionSupported()) {
glfwSetInputMode(editorWindow, GLFW_RAW_MOUSE_MOTION, GLFW_FALSE);
}
}
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
@@ -295,12 +284,13 @@ void Engine::run() {
handleKeyboardShortcuts();
}
viewportController.update(editorWindow, cursorLocked);
if (!viewportController.isViewportFocused() && cursorLocked) {
if (gameViewCursorLocked) {
cursorLocked = false;
glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
camera.firstMouse = true;
viewportController.setFocused(false);
}
viewportController.update(editorWindow, cursorLocked);
if (!isPlaying) {
gameViewCursorLocked = false;
}
// Scroll-wheel speed adjustment while freelook is active
@@ -318,9 +308,21 @@ void Engine::run() {
camera.processKeyboard(deltaTime, editorWindow);
}
// Run script tick/update even when the object is not selected.
// Run scripts only in play/spec/test modes to avoid edit-time side effects (e.g., cursor grabs)
if (projectManager.currentProject.isLoaded) {
updateScripts(deltaTime);
bool runScripts = isPlaying || specMode || testMode;
if (runScripts) {
updateScripts(deltaTime);
}
}
bool simulatePhysics = physics.isReady() && ((isPlaying && !isPaused) || (!isPlaying && specMode));
if (simulatePhysics) {
physics.simulate(deltaTime, sceneObjects);
}
if (isPlaying) {
updatePlayerController(deltaTime);
}
if (!showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) {
@@ -329,9 +331,19 @@ void Engine::run() {
if (aspect <= 0.0f) aspect = 1.0f;
glm::mat4 proj = glm::perspective(glm::radians(FOV), aspect, NEAR_PLANE, FAR_PLANE);
#ifdef GL_POLYGON_MODE
GLint prevPoly[2] = { GL_FILL, GL_FILL };
glGetIntegerv(GL_POLYGON_MODE, prevPoly);
glPolygonMode(GL_FRONT_AND_BACK, collisionWireframe ? GL_LINE : GL_FILL);
#endif
renderer.beginRender(view, proj, camera.position);
renderer.renderScene(camera, sceneObjects, selectedObjectId);
renderer.endRender();
#ifdef GL_POLYGON_MODE
glPolygonMode(GL_FRONT_AND_BACK, prevPoly[0]);
#endif
}
if (firstFrame) {
@@ -367,6 +379,7 @@ void Engine::run() {
}
renderViewport();
if (showGameViewport) renderGameViewportWindow();
renderDialogs();
}
@@ -391,6 +404,18 @@ void Engine::run() {
glfwMakeContextCurrent(backup_current_context);
}
// Enforce cursor lock state at the end of the frame based on latest flags.
bool anyLock = cursorLocked || gameViewCursorLocked;
int desiredMode = anyLock ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL;
if (glfwGetInputMode(editorWindow, GLFW_CURSOR) != desiredMode) {
glfwSetInputMode(editorWindow, GLFW_CURSOR, desiredMode);
if (anyLock && glfwRawMouseMotionSupported()) {
glfwSetInputMode(editorWindow, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
} else if (!anyLock && glfwRawMouseMotionSupported()) {
glfwSetInputMode(editorWindow, GLFW_RAW_MOUSE_MOTION, GLFW_FALSE);
}
}
glfwSwapBuffers(editorWindow);
if (firstFrame) {
@@ -407,6 +432,9 @@ void Engine::shutdown() {
saveCurrentScene();
}
physics.onPlayStop();
physics.shutdown();
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
@@ -656,7 +684,11 @@ void Engine::handleKeyboardShortcuts() {
ctrlNPressed = false;
}
bool cameraActive = cursorLocked || viewportController.isViewportFocused() && cursorLocked;
bool cameraActive = cursorLocked || (viewportController.isViewportFocused() && cursorLocked);
if (!isPlaying && gameViewCursorLocked) {
// Prevent edit-mode freelook from conflicting with game view capture
gameViewCursorLocked = false;
}
if (!cameraActive) {
if (ImGui::IsKeyPressed(ImGuiKey_Q)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
if (ImGui::IsKeyPressed(ImGuiKey_W)) mCurrentGizmoOperation = ImGuizmo::ROTATE;
@@ -669,6 +701,11 @@ void Engine::handleKeyboardShortcuts() {
}
}
if (ImGui::IsKeyPressed(ImGuiKey_3)) {
collisionWireframe = !collisionWireframe;
addConsoleMessage(std::string("Collision wireframe ") + (collisionWireframe ? "enabled" : "disabled"), ConsoleMessageType::Info);
}
static bool snapPressed = false;
static bool snapHeldByCtrl = false;
static bool snapStateBeforeCtrl = false;
@@ -709,6 +746,10 @@ void Engine::handleKeyboardShortcuts() {
if (glfwGetKey(editorWindow, GLFW_KEY_Y) == GLFW_RELEASE) {
redoPressed = false;
}
if (ImGui::IsKeyPressed(ImGuiKey_Escape) && gameViewCursorLocked) {
gameViewCursorLocked = false;
}
}
void Engine::updateScripts(float delta) {
@@ -729,6 +770,113 @@ void Engine::updateScripts(float delta) {
}
}
void Engine::updatePlayerController(float delta) {
if (!isPlaying) return;
SceneObject* player = nullptr;
for (auto& obj : sceneObjects) {
if (obj.hasPlayerController && obj.playerController.enabled) {
player = &obj;
activePlayerId = obj.id;
break;
}
}
if (!player) {
activePlayerId = -1;
return;
}
auto& pc = player->playerController;
// Maintain capsule sizing and collider defaults
if (pc.pitch == 0.0f && pc.yaw == 0.0f && (glm::length(player->rotation) > 0.01f)) {
pc.pitch = player->rotation.x;
pc.yaw = player->rotation.y;
}
player->hasCollider = true;
player->collider.type = ColliderType::Capsule;
player->collider.convex = true;
player->collider.boxSize = glm::vec3(pc.radius * 2.0f, pc.height, pc.radius * 2.0f);
player->hasRigidbody = true;
player->rigidbody.enabled = true;
player->rigidbody.useGravity = true;
player->rigidbody.isKinematic = false;
// Mouse look when game viewport is focused
if (gameViewportFocused || gameViewCursorLocked) {
ImGuiIO& io = ImGui::GetIO();
pc.yaw -= io.MouseDelta.x * 50.0f * pc.lookSensitivity * delta;
pc.pitch -= io.MouseDelta.y * 50.0f * pc.lookSensitivity * delta;
pc.pitch = std::clamp(pc.pitch, -89.0f, 89.0f);
}
// Movement input aligned to camera facing (-Z forward convention)
auto key = [&](int k) { return glfwGetKey(editorWindow, k) == GLFW_PRESS; };
glm::quat q = glm::quat(glm::radians(glm::vec3(pc.pitch, pc.yaw, 0.0f)));
glm::vec3 forward = glm::normalize(q * glm::vec3(0.0f, 0.0f, -1.0f));
glm::vec3 right = glm::normalize(q * glm::vec3(1.0f, 0.0f, 0.0f));
glm::vec3 planarForward = glm::normalize(glm::vec3(forward.x, 0.0f, forward.z));
glm::vec3 planarRight = glm::normalize(glm::vec3(right.x, 0.0f, right.z));
if (!std::isfinite(planarForward.x) || glm::length(planarForward) < 1e-3f) {
planarForward = glm::vec3(0, 0, -1);
}
if (!std::isfinite(planarRight.x) || glm::length(planarRight) < 1e-3f) {
planarRight = glm::vec3(1, 0, 0);
}
glm::vec3 move(0.0f);
if (key(GLFW_KEY_W)) move += planarForward;
if (key(GLFW_KEY_S)) move -= planarForward;
if (key(GLFW_KEY_D)) move += planarRight;
if (key(GLFW_KEY_A)) move -= planarRight;
if (glm::length(move) > 0.001f) move = glm::normalize(move);
float targetSpeed = pc.moveSpeed;
glm::vec3 velocity(move * targetSpeed);
// Simple gravity and jump
float capsuleHalf = std::max(0.1f, pc.height * 0.5f);
glm::vec3 physVel;
bool havePhysVel = physics.getLinearVelocity(player->id, physVel);
if (havePhysVel) pc.verticalVelocity = physVel.y;
// Ground check via PhysX scene query so mesh colliders work, not just the plane
glm::vec3 hitPos;
glm::vec3 hitNormal;
float hitDist = 0.0f;
float probeDist = capsuleHalf + 0.4f;
glm::vec3 rayStart = player->position + glm::vec3(0.0f, 0.1f, 0.0f);
bool hitGround = physics.raycastClosest(rayStart, glm::vec3(0.0f, -1.0f, 0.0f), probeDist,
player->id, &hitPos, &hitNormal, &hitDist);
bool grounded = hitGround && hitNormal.y > 0.25f && hitDist <= capsuleHalf + 0.2f && pc.verticalVelocity <= 0.35f;
if (!hitGround) {
// Fallback to simple height check to avoid regressions if queries fail
grounded = player->position.y <= capsuleHalf + 0.12f && pc.verticalVelocity <= 0.35f;
}
if (grounded) {
pc.verticalVelocity = 0.0f;
if (hitGround) {
player->position.y = std::max(player->position.y, hitPos.y + capsuleHalf);
} else {
player->position.y = capsuleHalf;
}
if (key(GLFW_KEY_SPACE)) {
pc.verticalVelocity = pc.jumpStrength;
}
} else {
pc.verticalVelocity += -9.81f * delta;
}
velocity.y = pc.verticalVelocity;
velocity.y = std::clamp(velocity.y, -30.0f, 30.0f);
// Apply yaw to physics actor and keep collider aligned
physics.setActorYaw(player->id, pc.yaw);
player->rotation = glm::vec3(pc.pitch, pc.yaw, 0.0f);
if (!physics.setLinearVelocity(player->id, velocity)) {
player->position += velocity * delta;
}
}
void Engine::OpenProjectPath(const std::string& path) {
try {
if (projectManager.loadProject(path)) {
@@ -746,6 +894,10 @@ void Engine::OpenProjectPath(const std::string& path) {
return;
}
if (!physics.isReady() && !physics.init()) {
addConsoleMessage("Warning: PhysX failed to initialize; physics disabled for this session", ConsoleMessageType::Warning);
}
loadRecentScenes();
fileBrowser.setProjectRoot(projectManager.currentProject.projectPath);
fileBrowser.currentPath = projectManager.currentProject.projectPath;
@@ -779,6 +931,10 @@ void Engine::createNewProject(const char* name, const char* location) {
return;
}
if (!physics.isReady() && !physics.init()) {
addConsoleMessage("Warning: PhysX failed to initialize; physics disabled for this session", ConsoleMessageType::Warning);
}
sceneObjects.clear();
clearSelection();
nextObjectId = 0;
@@ -950,6 +1106,12 @@ void Engine::duplicateSelected() {
newObj.light = it->light;
newObj.camera = it->camera;
newObj.postFx = it->postFx;
newObj.hasRigidbody = it->hasRigidbody;
newObj.rigidbody = it->rigidbody;
newObj.hasCollider = it->hasCollider;
newObj.collider = it->collider;
newObj.hasPlayerController = it->hasPlayerController;
newObj.playerController = it->playerController;
sceneObjects.push_back(newObj);
setPrimarySelection(id);
@@ -1071,6 +1233,18 @@ void Engine::markProjectDirty() {
projectManager.currentProject.hasUnsavedChanges = true;
}
bool Engine::setRigidbodyVelocityFromScript(int id, const glm::vec3& velocity) {
return physics.setLinearVelocity(id, velocity);
}
bool Engine::getRigidbodyVelocityFromScript(int id, glm::vec3& outVelocity) {
return physics.getLinearVelocity(id, outVelocity);
}
bool Engine::teleportPhysicsActorFromScript(int id, const glm::vec3& position, const glm::vec3& rotationDeg) {
return physics.setActorPose(id, position, rotationDeg);
}
void Engine::compileScriptFile(const fs::path& scriptPath) {
if (!projectManager.currentProject.isLoaded) {
addConsoleMessage("No project is loaded", ConsoleMessageType::Warning);

View File

@@ -9,6 +9,7 @@
#include "MeshBuilder.h"
#include "ScriptCompiler.h"
#include "ScriptRuntime.h"
#include "PhysicsSystem.h"
#include "../include/Window/Window.h"
void window_size_callback(GLFWwindow* window, int width, int height);
@@ -90,7 +91,11 @@ private:
bool isPlaying = false;
bool isPaused = false;
bool showViewOutput = true;
bool showGameViewport = true;
int previewCameraId = -1;
bool gameViewCursorLocked = false;
bool gameViewportFocused = false;
int activePlayerId = -1;
MeshBuilder meshBuilder;
char meshBuilderPath[260] = "";
char meshBuilderFaceInput[128] = "";
@@ -105,12 +110,14 @@ private:
MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Vertex;
ScriptCompiler scriptCompiler;
ScriptRuntime scriptRuntime;
PhysicsSystem physics;
bool showCompilePopup = false;
bool lastCompileSuccess = false;
std::string lastCompileStatus;
std::string lastCompileLog;
bool specMode = false;
bool testMode = false;
bool collisionWireframe = false;
// Private methods
SceneObject* getSelectedObject();
@@ -141,11 +148,13 @@ private:
void renderInspectorPanel();
void renderConsolePanel();
void renderViewport();
void renderGameViewportWindow();
void renderDialogs();
void renderProjectBrowserPanel();
Camera makeCameraFromObject(const SceneObject& obj) const;
void compileScriptFile(const fs::path& scriptPath);
void updateScripts(float delta);
void updatePlayerController(float delta);
void renderFileBrowserToolbar();
void renderFileBrowserBreadcrumb();
@@ -206,4 +215,8 @@ public:
void markProjectDirty();
// Script-accessible logging wrapper
void addConsoleMessageFromScript(const std::string& message, ConsoleMessageType type);
// Script-accessible physics helpers
bool setRigidbodyVelocityFromScript(int id, const glm::vec3& velocity);
bool getRigidbodyVelocityFromScript(int id, glm::vec3& outVelocity);
bool teleportPhysicsActorFromScript(int id, const glm::vec3& position, const glm::vec3& rotationDeg);
};

View File

@@ -296,6 +296,60 @@ namespace FileIcons {
}
}
void Engine::renderGameViewportWindow() {
gameViewportFocused = false;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 6.0f));
ImGui::Begin("Game Viewport", &showGameViewport, ImGuiWindowFlags_NoScrollbar);
bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
ImVec2 avail = ImGui::GetContentRegionAvail();
int width = std::max(160, (int)avail.x);
int height = std::max(120, (int)avail.y);
const SceneObject* playerCam = nullptr;
for (const auto& obj : sceneObjects) {
if (obj.type == ObjectType::Camera && obj.camera.type == SceneCameraType::Player) {
playerCam = &obj;
break;
}
}
if (!isPlaying) {
gameViewCursorLocked = false;
}
if (playerCam && rendererInitialized) {
unsigned int tex = renderer.renderScenePreview(
makeCameraFromObject(*playerCam),
sceneObjects,
width,
height,
playerCam->camera.fov,
playerCam->camera.nearClip,
playerCam->camera.farClip
);
ImGui::Image((void*)(intptr_t)tex, ImVec2((float)width, (float)height), ImVec2(0, 1), ImVec2(1, 0));
bool hovered = ImGui::IsItemHovered();
bool clicked = hovered && isPlaying && ImGui::IsMouseClicked(ImGuiMouseButton_Left);
if (clicked && !gameViewCursorLocked) {
gameViewCursorLocked = true;
}
if (gameViewCursorLocked && (!isPlaying || !windowFocused || ImGui::IsKeyPressed(ImGuiKey_Escape))) {
gameViewCursorLocked = false;
}
gameViewportFocused = windowFocused && gameViewCursorLocked;
ImGui::TextDisabled(gameViewCursorLocked ? "Camera captured (ESC to release)" : "Click to capture");
} else {
ImGui::TextDisabled("No player camera found (Camera Type: Player).");
gameViewportFocused = ImGui::IsWindowFocused();
}
ImGui::End();
ImGui::PopStyleVar();
}
void Engine::renderFileBrowserPanel() {
ImGui::Begin("Project", &showFileBrowser);
ImGuiStyle& style = ImGui::GetStyle();
@@ -1307,7 +1361,23 @@ void Engine::renderMainMenuBar() {
}
if (ImGui::BeginMenu("Scripts")) {
ImGui::MenuItem("Spec Mode (run Script_Spec)", nullptr, &specMode);
auto toggleSpec = [&](bool enabled) {
if (specMode == enabled) return;
if (enabled && !physics.isReady() && !physics.init()) {
addConsoleMessage("PhysX failed to initialize; spec mode disabled", ConsoleMessageType::Warning);
specMode = false;
return;
}
specMode = enabled;
if (!isPlaying) {
if (specMode) physics.onPlayStart(sceneObjects);
else physics.onPlayStop();
}
};
bool specValue = specMode;
if (ImGui::MenuItem("Spec Mode (run Script_Spec)", nullptr, &specValue)) {
toggleSpec(specValue);
}
ImGui::MenuItem("Test Mode (run Script_TestEditor)", nullptr, &testMode);
ImGui::EndMenu();
}
@@ -1353,18 +1423,43 @@ void Engine::renderMainMenuBar() {
bool playPressed = ImGui::Button(isPlaying ? "Stop" : "Play");
ImGui::SameLine(0.0f, 6.0f);
bool pausePressed = ImGui::Button(isPaused ? "Resume" : "Pause");
ImGui::SameLine(0.0f, 6.0f);
bool specPressed = ImGui::Button(specMode ? "Spec On" : "Spec Mode");
ImGui::PopStyleVar();
if (playPressed) {
isPlaying = !isPlaying;
if (!isPlaying) {
bool newState = !isPlaying;
if (newState) {
if (physics.isReady() || physics.init()) {
physics.onPlayStart(sceneObjects);
} else {
addConsoleMessage("PhysX failed to initialize; physics disabled for play mode", ConsoleMessageType::Warning);
}
} else {
physics.onPlayStop();
isPaused = false;
if (specMode && (physics.isReady() || physics.init())) {
physics.onPlayStart(sceneObjects);
}
}
isPlaying = newState;
}
if (pausePressed) {
isPaused = !isPaused;
if (isPaused) isPlaying = true; // placeholder: pausing implies were in play mode
}
if (specPressed) {
bool enable = !specMode;
if (enable && !physics.isReady() && !physics.init()) {
addConsoleMessage("PhysX failed to initialize; spec mode disabled", ConsoleMessageType::Warning);
enable = false;
}
specMode = enable;
if (!isPlaying) {
if (specMode) physics.onPlayStart(sceneObjects);
else physics.onPlayStop();
}
}
float rightX = ImGui::GetWindowWidth() - 220.0f;
if (rightX > ImGui::GetCursorPosX()) {
@@ -1882,6 +1977,170 @@ void Engine::renderInspectorPanel() {
ImGui::PopStyleColor();
if (obj.hasCollider) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.5f, 0.35f, 1.0f));
if (ImGui::CollapsingHeader("Collider", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent(10.0f);
bool changed = false;
if (ImGui::Checkbox("Enabled", &obj.collider.enabled)) {
changed = true;
}
const char* colliderTypes[] = { "Box", "Mesh", "Convex Mesh", "Capsule" };
int colliderType = static_cast<int>(obj.collider.type);
if (ImGui::Combo("Type", &colliderType, colliderTypes, IM_ARRAYSIZE(colliderTypes))) {
obj.collider.type = static_cast<ColliderType>(colliderType);
changed = true;
}
if (obj.collider.type == ColliderType::Box) {
if (ImGui::DragFloat3("Box Size", &obj.collider.boxSize.x, 0.01f, 0.01f, 1000.0f, "%.3f")) {
obj.collider.boxSize.x = std::max(0.01f, obj.collider.boxSize.x);
obj.collider.boxSize.y = std::max(0.01f, obj.collider.boxSize.y);
obj.collider.boxSize.z = std::max(0.01f, obj.collider.boxSize.z);
changed = true;
}
if (ImGui::SmallButton("Match Object Scale")) {
obj.collider.boxSize = glm::max(obj.scale, glm::vec3(0.01f));
changed = true;
}
} else if (obj.collider.type == ColliderType::Capsule) {
float radius = std::max(0.05f, std::max(obj.collider.boxSize.x, obj.collider.boxSize.z) * 0.5f);
float height = std::max(0.1f, obj.collider.boxSize.y);
if (ImGui::DragFloat("Radius", &radius, 0.01f, 0.05f, 5.0f, "%.3f")) {
obj.collider.boxSize.x = obj.collider.boxSize.z = radius * 2.0f;
changed = true;
}
if (ImGui::DragFloat("Height", &height, 0.01f, 0.1f, 10.0f, "%.3f")) {
obj.collider.boxSize.y = height;
changed = true;
}
ImGui::TextDisabled("Capsule aligned to Y axis.");
} else {
if (ImGui::Checkbox("Use Convex Hull (required for Rigidbody)", &obj.collider.convex)) {
changed = true;
}
ImGui::TextDisabled("Uses mesh from the object (OBJ/Model). Non-convex is static-only.");
}
ImGui::Spacing();
if (ImGui::Button("Remove Collider", ImVec2(-1, 0))) {
obj.hasCollider = false;
changed = true;
}
if (changed) {
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::Unindent(10.0f);
}
ImGui::PopStyleColor();
}
if (obj.hasPlayerController) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.45f, 0.7f, 1.0f));
if (ImGui::CollapsingHeader("Player Controller", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent(10.0f);
bool changed = false;
if (ImGui::Checkbox("Enabled", &obj.playerController.enabled)) {
changed = true;
}
if (ImGui::DragFloat("Move Speed", &obj.playerController.moveSpeed, 0.1f, 0.1f, 100.0f, "%.2f")) {
obj.playerController.moveSpeed = std::max(0.1f, obj.playerController.moveSpeed);
changed = true;
}
if (ImGui::DragFloat("Look Sensitivity", &obj.playerController.lookSensitivity, 0.01f, 0.01f, 2.0f, "%.2f")) {
obj.playerController.lookSensitivity = std::clamp(obj.playerController.lookSensitivity, 0.01f, 2.0f);
changed = true;
}
if (ImGui::DragFloat("Height", &obj.playerController.height, 0.01f, 0.5f, 3.0f, "%.2f")) {
obj.playerController.height = std::clamp(obj.playerController.height, 0.5f, 3.0f);
obj.scale.y = obj.playerController.height;
obj.collider.boxSize.y = obj.playerController.height;
changed = true;
}
if (ImGui::DragFloat("Radius", &obj.playerController.radius, 0.01f, 0.2f, 1.2f, "%.2f")) {
obj.playerController.radius = std::clamp(obj.playerController.radius, 0.2f, 1.2f);
obj.scale.x = obj.scale.z = obj.playerController.radius * 2.0f;
obj.collider.boxSize.x = obj.collider.boxSize.z = obj.playerController.radius * 2.0f;
changed = true;
}
if (ImGui::DragFloat("Jump Strength", &obj.playerController.jumpStrength, 0.1f, 0.1f, 30.0f, "%.1f")) {
obj.playerController.jumpStrength = std::max(0.1f, obj.playerController.jumpStrength);
changed = true;
}
if (ImGui::Button("Remove Player Controller", ImVec2(-1, 0))) {
obj.hasPlayerController = false;
changed = true;
}
if (changed) {
obj.hasCollider = true;
obj.collider.type = ColliderType::Capsule;
obj.collider.convex = true;
obj.hasRigidbody = true;
obj.rigidbody.enabled = true;
obj.rigidbody.useGravity = true;
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::Unindent(10.0f);
}
ImGui::PopStyleColor();
}
if (obj.hasRigidbody) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.45f, 0.25f, 1.0f));
if (ImGui::CollapsingHeader("Rigidbody", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent(10.0f);
bool changed = false;
if (ImGui::Checkbox("Enabled", &obj.rigidbody.enabled)) {
changed = true;
}
ImGui::SameLine();
ImGui::BeginDisabled(true);
ImGui::Checkbox("Collider (mesh type)", &obj.rigidbody.enabled); // placeholder label to hint geometry from mesh
ImGui::EndDisabled();
if (ImGui::DragFloat("Mass", &obj.rigidbody.mass, 0.05f, 0.01f, 1000.0f, "%.2f")) {
obj.rigidbody.mass = std::max(0.01f, obj.rigidbody.mass);
changed = true;
}
if (ImGui::Checkbox("Use Gravity", &obj.rigidbody.useGravity)) {
changed = true;
}
if (ImGui::Checkbox("Kinematic", &obj.rigidbody.isKinematic)) {
changed = true;
}
if (ImGui::DragFloat("Linear Damping", &obj.rigidbody.linearDamping, 0.01f, 0.0f, 10.0f)) {
obj.rigidbody.linearDamping = std::clamp(obj.rigidbody.linearDamping, 0.0f, 10.0f);
changed = true;
}
if (ImGui::DragFloat("Angular Damping", &obj.rigidbody.angularDamping, 0.01f, 0.0f, 10.0f)) {
obj.rigidbody.angularDamping = std::clamp(obj.rigidbody.angularDamping, 0.0f, 10.0f);
changed = true;
}
ImGui::Spacing();
if (ImGui::Button("Remove Rigidbody", ImVec2(-1, 0))) {
obj.hasRigidbody = false;
changed = true;
}
if (changed) {
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::Unindent(10.0f);
}
ImGui::PopStyleColor();
}
if (obj.type == ObjectType::Camera) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.35f, 0.65f, 1.0f));
@@ -2230,6 +2489,51 @@ void Engine::renderInspectorPanel() {
ImGui::OpenPopup("AddComponentPopup");
}
if (ImGui::BeginPopup("AddComponentPopup")) {
if (!obj.hasRigidbody && ImGui::MenuItem("Rigidbody")) {
obj.hasRigidbody = true;
obj.rigidbody = RigidbodyComponent{};
materialChanged = true;
}
if (!obj.hasPlayerController && ImGui::MenuItem("Player Controller")) {
obj.hasPlayerController = true;
obj.playerController = PlayerControllerComponent{};
obj.hasCollider = true;
obj.collider.type = ColliderType::Capsule;
obj.collider.boxSize = glm::vec3(obj.playerController.radius * 2.0f, obj.playerController.height, obj.playerController.radius * 2.0f);
obj.collider.convex = true;
obj.hasRigidbody = true;
obj.rigidbody.enabled = true;
obj.rigidbody.useGravity = true;
obj.rigidbody.isKinematic = false;
obj.scale = glm::vec3(obj.playerController.radius * 2.0f, obj.playerController.height, obj.playerController.radius * 2.0f);
materialChanged = true;
}
if (!obj.hasCollider && ImGui::BeginMenu("Collider")) {
if (ImGui::MenuItem("Box Collider")) {
obj.hasCollider = true;
obj.collider = ColliderComponent{};
obj.collider.boxSize = glm::max(obj.scale, glm::vec3(0.01f));
materialChanged = true;
addComponentButtonShown = true;
}
if (ImGui::MenuItem("Mesh Collider (Triangle)")) {
obj.hasCollider = true;
obj.collider = ColliderComponent{};
obj.collider.type = ColliderType::Mesh;
obj.collider.convex = false;
materialChanged = true;
addComponentButtonShown = true;
}
if (ImGui::MenuItem("Mesh Collider (Convex)")) {
obj.hasCollider = true;
obj.collider = ColliderComponent{};
obj.collider.type = ColliderType::ConvexMesh;
obj.collider.convex = true;
materialChanged = true;
addComponentButtonShown = true;
}
ImGui::EndMenu();
}
if (ImGui::MenuItem("Script")) {
obj.scripts.push_back(ScriptComponent{});
materialChanged = true;
@@ -2435,6 +2739,52 @@ void Engine::renderInspectorPanel() {
ImGui::OpenPopup("AddComponentPopup");
}
if (ImGui::BeginPopup("AddComponentPopup")) {
if (!obj.hasRigidbody && ImGui::MenuItem("Rigidbody")) {
obj.hasRigidbody = true;
obj.rigidbody = RigidbodyComponent{};
projectManager.currentProject.hasUnsavedChanges = true;
}
if (!obj.hasPlayerController && ImGui::MenuItem("Player Controller")) {
obj.hasPlayerController = true;
obj.playerController = PlayerControllerComponent{};
obj.hasCollider = true;
obj.collider.type = ColliderType::Capsule;
obj.collider.boxSize = glm::vec3(obj.playerController.radius * 2.0f, obj.playerController.height, obj.playerController.radius * 2.0f);
obj.collider.convex = true;
obj.hasRigidbody = true;
obj.rigidbody.enabled = true;
obj.rigidbody.useGravity = true;
obj.rigidbody.isKinematic = false;
obj.scale = glm::vec3(obj.playerController.radius * 2.0f, obj.playerController.height, obj.playerController.radius * 2.0f);
projectManager.currentProject.hasUnsavedChanges = true;
addComponentButtonShown = true;
}
if (!obj.hasCollider && ImGui::BeginMenu("Collider")) {
if (ImGui::MenuItem("Box Collider")) {
obj.hasCollider = true;
obj.collider = ColliderComponent{};
obj.collider.boxSize = glm::max(obj.scale, glm::vec3(0.01f));
projectManager.currentProject.hasUnsavedChanges = true;
addComponentButtonShown = true;
}
if (ImGui::MenuItem("Mesh Collider (Triangle)")) {
obj.hasCollider = true;
obj.collider = ColliderComponent{};
obj.collider.type = ColliderType::Mesh;
obj.collider.convex = false;
projectManager.currentProject.hasUnsavedChanges = true;
addComponentButtonShown = true;
}
if (ImGui::MenuItem("Mesh Collider (Convex)")) {
obj.hasCollider = true;
obj.collider = ColliderComponent{};
obj.collider.type = ColliderType::ConvexMesh;
obj.collider.convex = true;
projectManager.currentProject.hasUnsavedChanges = true;
addComponentButtonShown = true;
}
ImGui::EndMenu();
}
if (ImGui::MenuItem("Script")) {
obj.scripts.push_back(ScriptComponent{});
projectManager.currentProject.hasUnsavedChanges = true;
@@ -3919,19 +4269,11 @@ void Engine::renderViewport() {
if (mouseOverViewportImage && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
viewportController.setFocused(true);
cursorLocked = true;
glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
if (glfwRawMouseMotionSupported()) {
glfwSetInputMode(editorWindow, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
}
camera.firstMouse = true;
}
if (cursorLocked && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
cursorLocked = false;
glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
if (glfwRawMouseMotionSupported()) {
glfwSetInputMode(editorWindow, GLFW_RAW_MOUSE_MOTION, GLFW_FALSE);
}
camera.firstMouse = true;
}
if (cursorLocked) {

529
src/PhysicsSystem.cpp Normal file
View File

@@ -0,0 +1,529 @@
#include "PhysicsSystem.h"
#ifdef MODULARITY_ENABLE_PHYSX
#include "PxPhysicsAPI.h"
#include "ModelLoader.h"
#include <numeric>
#include <algorithm>
#include "extensions/PxRigidBodyExt.h"
using namespace physx;
namespace {
PxVec3 ToPxVec3(const glm::vec3& v) {
return PxVec3(v.x, v.y, v.z);
}
PxQuat ToPxQuat(const glm::vec3& eulerDeg) {
glm::vec3 radians = glm::radians(eulerDeg);
glm::quat q = glm::quat(radians);
return PxQuat(q.x, q.y, q.z, q.w);
}
glm::vec3 ToGlmVec3(const PxVec3& v) {
return glm::vec3(v.x, v.y, v.z);
}
glm::vec3 ToGlmEulerDeg(const PxQuat& q) {
glm::quat gq(q.w, q.x, q.y, q.z);
return glm::degrees(glm::eulerAngles(gq));
}
} // namespace
namespace {
struct IgnoreActorFilter : PxQueryFilterCallback {
PxRigidActor* ignore = nullptr;
explicit IgnoreActorFilter(PxRigidActor* actor) : ignore(actor) {}
PxQueryHitType::Enum preFilter(const PxFilterData&,
const PxShape* shape,
const PxRigidActor* actor,
PxHitFlags&) override {
if (actor == ignore) return PxQueryHitType::eNONE;
// Keep default blocking behaviour
if (shape && shape->getFlags().isSet(PxShapeFlag::eTRIGGER_SHAPE)) {
return PxQueryHitType::eNONE;
}
return PxQueryHitType::eBLOCK;
}
PxQueryHitType::Enum postFilter(const PxFilterData&,
const PxQueryHit&,
const PxShape*,
const PxRigidActor*) override {
return PxQueryHitType::eBLOCK;
}
};
} // namespace
bool PhysicsSystem::init() {
if (isReady()) return true;
mFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, mAllocator, mErrorCallback);
if (!mFoundation) return false;
mPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *mFoundation, PxTolerancesScale(), true, nullptr);
if (!mPhysics) return false;
mDispatcher = PxDefaultCpuDispatcherCreate(2);
if (!mDispatcher) return false;
PxTolerancesScale scale = mPhysics->getTolerancesScale();
mCookParams = PxCookingParams(scale);
mCookParams.meshPreprocessParams |= PxMeshPreprocessingFlag::eDISABLE_ACTIVE_EDGES_PRECOMPUTE;
mCookParams.meshPreprocessParams |= PxMeshPreprocessingFlag::eWELD_VERTICES;
PxSceneDesc sceneDesc(mPhysics->getTolerancesScale());
sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
sceneDesc.cpuDispatcher = mDispatcher;
sceneDesc.filterShader = PxDefaultSimulationFilterShader;
sceneDesc.flags |= PxSceneFlag::eENABLE_CCD;
mScene = mPhysics->createScene(sceneDesc);
if (!mScene) return false;
mDefaultMaterial = mPhysics->createMaterial(0.9f, 0.9f, 0.0f);
return mDefaultMaterial != nullptr;
}
bool PhysicsSystem::isReady() const {
return mFoundation && mPhysics && mScene && mDefaultMaterial;
}
void PhysicsSystem::createGroundPlane() {
if (!isReady()) return;
if (mGroundPlane) {
mScene->removeActor(*mGroundPlane);
mGroundPlane->release();
mGroundPlane = nullptr;
}
mGroundPlane = PxCreatePlane(*mPhysics, PxPlane(0.0f, 1.0f, 0.0f, 0.0f), *mDefaultMaterial);
if (mGroundPlane) {
mScene->addActor(*mGroundPlane);
}
}
bool PhysicsSystem::gatherMeshData(const SceneObject& obj, std::vector<PxVec3>& vertices, std::vector<uint32_t>& indices) const {
const OBJLoader::LoadedMesh* meshInfo = nullptr;
if (obj.type == ObjectType::OBJMesh && obj.meshId >= 0) {
meshInfo = g_objLoader.getMeshInfo(obj.meshId);
} else if (obj.type == ObjectType::Model && obj.meshId >= 0) {
meshInfo = getModelLoader().getMeshInfo(obj.meshId);
}
if (!meshInfo || meshInfo->triangleVertices.empty()) {
return false;
}
vertices.reserve(meshInfo->triangleVertices.size());
indices.resize(meshInfo->triangleVertices.size());
for (size_t i = 0; i < meshInfo->triangleVertices.size(); ++i) {
const glm::vec3& v = meshInfo->triangleVertices[i];
vertices.emplace_back(v.x, v.y, v.z);
indices[i] = static_cast<uint32_t>(i);
}
return !vertices.empty() && (indices.size() % 3 == 0);
}
PxTriangleMesh* PhysicsSystem::cookTriangleMesh(const std::vector<PxVec3>& vertices,
const std::vector<uint32_t>& indices) const {
if (vertices.empty() || indices.size() < 3) return nullptr;
PxTriangleMeshDesc desc;
desc.points.count = static_cast<uint32_t>(vertices.size());
desc.points.stride = sizeof(PxVec3);
desc.points.data = vertices.data();
desc.triangles.count = static_cast<uint32_t>(indices.size() / 3);
desc.triangles.stride = 3 * sizeof(uint32_t);
desc.triangles.data = indices.data();
PxDefaultMemoryOutputStream buf;
if (!PxCookTriangleMesh(mCookParams, desc, buf)) {
return nullptr;
}
PxDefaultMemoryInputData input(buf.getData(), buf.getSize());
return mPhysics->createTriangleMesh(input);
}
PxConvexMesh* PhysicsSystem::cookConvexMesh(const std::vector<PxVec3>& vertices) const {
if (vertices.size() < 4) return nullptr;
PxConvexMeshDesc desc;
desc.points.count = static_cast<uint32_t>(vertices.size());
desc.points.stride = sizeof(PxVec3);
desc.points.data = vertices.data();
desc.flags = PxConvexFlag::eCOMPUTE_CONVEX | PxConvexFlag::eCHECK_ZERO_AREA_TRIANGLES;
desc.vertexLimit = 255;
PxDefaultMemoryOutputStream buf;
if (!PxCookConvexMesh(mCookParams, desc, buf)) {
return nullptr;
}
PxDefaultMemoryInputData input(buf.getData(), buf.getSize());
return mPhysics->createConvexMesh(input);
}
bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject& obj, bool isDynamic) const {
(void)isDynamic;
if (!actor) return false;
PxShape* shape = nullptr;
auto tuneShape = [](PxShape* s, float minDim, bool /*swept*/) {
if (!s) return;
float contact = std::clamp(minDim * 0.2f, 0.02f, 0.2f);
float rest = contact * 0.15f;
s->setContactOffset(contact);
s->setRestOffset(rest);
};
switch (obj.type) {
case ObjectType::Cube: {
PxVec3 halfExtents = ToPxVec3(glm::max(obj.scale * 0.5f, glm::vec3(0.01f)));
shape = mPhysics->createShape(PxBoxGeometry(halfExtents), *mDefaultMaterial, true);
tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic);
break;
}
case ObjectType::Sphere: {
float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f;
radius = std::max(radius, 0.01f);
shape = mPhysics->createShape(PxSphereGeometry(radius), *mDefaultMaterial, true);
tuneShape(shape, radius * 2.0f, isDynamic);
break;
}
case ObjectType::Capsule: {
float radius = std::max(obj.scale.x, obj.scale.z) * 0.5f;
radius = std::max(radius, 0.01f);
float cylHeight = std::max(0.05f, obj.scale.y - radius * 2.0f);
float halfHeight = cylHeight * 0.5f;
shape = mPhysics->createShape(PxCapsuleGeometry(radius, halfHeight), *mDefaultMaterial, true);
if (shape) {
// PhysX capsules default to the X axis; rotate to align with Y (character up)
shape->setLocalPose(PxTransform(PxQuat(PxHalfPi, PxVec3(0, 0, 1))));
}
tuneShape(shape, std::min(radius * 2.0f, halfHeight * 2.0f), isDynamic);
break;
}
default:
break;
}
if (!shape) return false;
actor->attachShape(*shape);
shape->release();
return true;
}
bool PhysicsSystem::attachColliderShape(PxRigidActor* actor, const SceneObject& obj, bool isDynamic) const {
if (!actor || !obj.hasCollider || !obj.collider.enabled) return false;
PxShape* shape = nullptr;
auto tuneShape = [](PxShape* s, float minDim, bool /*swept*/) {
if (!s) return;
float contact = std::clamp(minDim * 0.12f, 0.015f, 0.12f);
float rest = contact * 0.2f;
s->setContactOffset(contact);
s->setRestOffset(rest);
};
float minDim = 0.1f;
if (obj.collider.type == ColliderType::Box) {
glm::vec3 half = glm::max(obj.collider.boxSize * 0.5f, glm::vec3(0.01f));
shape = mPhysics->createShape(PxBoxGeometry(ToPxVec3(half)), *mDefaultMaterial, true);
minDim = std::min({half.x, half.y, half.z}) * 2.0f;
} else if (obj.collider.type == ColliderType::Capsule) {
float radius = std::max({obj.collider.boxSize.x, obj.collider.boxSize.z}) * 0.5f;
radius = std::max(radius, 0.01f);
float cylHeight = std::max(0.05f, obj.collider.boxSize.y - radius * 2.0f);
float halfHeight = cylHeight * 0.5f;
shape = mPhysics->createShape(PxCapsuleGeometry(radius, halfHeight), *mDefaultMaterial, true);
if (shape) {
// Rotate capsule so its axis matches the engine's Y-up expectation
shape->setLocalPose(PxTransform(PxQuat(PxHalfPi, PxVec3(0, 0, 1))));
}
minDim = std::min(radius * 2.0f, halfHeight * 2.0f);
} else {
std::vector<PxVec3> verts;
std::vector<uint32_t> indices;
if (!gatherMeshData(obj, verts, indices)) {
return false;
}
bool useConvex = obj.collider.convex || obj.collider.type == ColliderType::ConvexMesh || isDynamic;
glm::vec3 boundsMin(FLT_MAX);
glm::vec3 boundsMax(-FLT_MAX);
for (auto& v : verts) {
boundsMin.x = std::min(boundsMin.x, v.x * obj.scale.x);
boundsMin.y = std::min(boundsMin.y, v.y * obj.scale.y);
boundsMin.z = std::min(boundsMin.z, v.z * obj.scale.z);
boundsMax.x = std::max(boundsMax.x, v.x * obj.scale.x);
boundsMax.y = std::max(boundsMax.y, v.y * obj.scale.y);
boundsMax.z = std::max(boundsMax.z, v.z * obj.scale.z);
}
minDim = std::max(0.01f, std::min({boundsMax.x - boundsMin.x, boundsMax.y - boundsMin.y, boundsMax.z - boundsMin.z}));
if (useConvex) {
PxConvexMesh* convex = cookConvexMesh(verts);
if (!convex) return false;
PxConvexMeshGeometry geom(convex, PxMeshScale(ToPxVec3(obj.scale), PxQuat(PxIdentity)));
shape = mPhysics->createShape(geom, *mDefaultMaterial, true);
convex->release();
} else {
PxTriangleMesh* tri = cookTriangleMesh(verts, indices);
if (!tri) return false;
PxTriangleMeshGeometry geom(tri, PxMeshScale(ToPxVec3(obj.scale), PxQuat(PxIdentity)));
shape = mPhysics->createShape(geom, *mDefaultMaterial, true);
tri->release();
}
}
tuneShape(shape, std::max(0.01f, minDim), isDynamic || obj.hasPlayerController);
if (!shape) return false;
actor->attachShape(*shape);
shape->release();
return true;
}
PhysicsSystem::ActorRecord PhysicsSystem::createActorFor(const SceneObject& obj) const {
ActorRecord record;
const bool wantsDynamic = obj.hasRigidbody && obj.rigidbody.enabled;
const bool wantsCollider = obj.hasCollider && obj.collider.enabled;
if (!wantsDynamic && !wantsCollider) {
return record;
}
PxTransform transform(ToPxVec3(obj.position), ToPxQuat(obj.rotation));
PxRigidActor* actor = wantsDynamic
? static_cast<PxRigidActor*>(mPhysics->createRigidDynamic(transform))
: static_cast<PxRigidActor*>(mPhysics->createRigidStatic(transform));
if (!actor) return record;
record.actor = actor;
record.isDynamic = wantsDynamic;
record.isKinematic = wantsDynamic && obj.rigidbody.isKinematic;
bool attached = false;
// Keep actor facing initial yaw (ignore pitch/roll)
if (PxRigidDynamic* dyn = actor->is<PxRigidDynamic>()) {
PxTransform pose = dyn->getGlobalPose();
pose.q = PxQuat(static_cast<float>(glm::radians(obj.rotation.y)), PxVec3(0, 1, 0));
dyn->setGlobalPose(pose);
} else {
PxTransform pose = actor->getGlobalPose();
pose.q = PxQuat(static_cast<float>(glm::radians(obj.rotation.y)), PxVec3(0, 1, 0));
actor->setGlobalPose(pose);
}
if (wantsCollider) {
attached = attachColliderShape(actor, obj, wantsDynamic);
}
if (!attached) {
attached = attachPrimitiveShape(actor, obj, wantsDynamic);
}
if (!attached) {
actor->release();
record.actor = nullptr;
return record;
}
if (PxRigidDynamic* dyn = actor->is<PxRigidDynamic>()) {
dyn->setAngularDamping(obj.rigidbody.angularDamping);
dyn->setLinearDamping(obj.rigidbody.linearDamping);
dyn->setRigidBodyFlag(PxRigidBodyFlag::eKINEMATIC, obj.rigidbody.isKinematic);
dyn->setActorFlag(PxActorFlag::eDISABLE_GRAVITY, !obj.rigidbody.useGravity);
dyn->setRigidDynamicLockFlags(PxRigidDynamicLockFlag::eLOCK_ANGULAR_X | PxRigidDynamicLockFlag::eLOCK_ANGULAR_Z);
if (obj.hasPlayerController) {
dyn->setRigidBodyFlag(PxRigidBodyFlag::eENABLE_CCD, true);
dyn->setMaxDepenetrationVelocity(1.5f);
}
if (!obj.rigidbody.isKinematic) {
PxRigidBodyExt::updateMassAndInertia(*dyn, std::max(0.01f, obj.rigidbody.mass));
}
}
return record;
}
void PhysicsSystem::clearActors() {
for (auto& [id, rec] : mActors) {
if (rec.actor && mScene) {
mScene->removeActor(*rec.actor);
rec.actor->release();
}
}
mActors.clear();
if (mGroundPlane && mScene) {
mScene->removeActor(*mGroundPlane);
mGroundPlane->release();
mGroundPlane = nullptr;
}
}
void PhysicsSystem::onPlayStart(const std::vector<SceneObject>& objects) {
if (!isReady()) return;
clearActors();
createGroundPlane();
for (const auto& obj : objects) {
ActorRecord rec = createActorFor(obj);
if (!rec.actor) continue;
mScene->addActor(*rec.actor);
mActors[obj.id] = rec;
}
}
void PhysicsSystem::onPlayStop() {
clearActors();
}
bool PhysicsSystem::setLinearVelocity(int id, const glm::vec3& velocity) {
#ifdef MODULARITY_ENABLE_PHYSX
auto it = mActors.find(id);
if (it == mActors.end()) return false;
ActorRecord& rec = it->second;
if (!rec.actor || !rec.isDynamic) return false;
if (PxRigidDynamic* dyn = rec.actor->is<PxRigidDynamic>()) {
dyn->setLinearVelocity(ToPxVec3(velocity));
return true;
}
#endif
return false;
}
bool PhysicsSystem::setActorYaw(int id, float yawDegrees) {
#ifdef MODULARITY_ENABLE_PHYSX
auto it = mActors.find(id);
if (it == mActors.end()) return false;
ActorRecord& rec = it->second;
if (!rec.actor) return false;
PxTransform pose = rec.actor->getGlobalPose();
PxQuat yawQuat(static_cast<float>(glm::radians(yawDegrees)), PxVec3(0, 1, 0));
pose.q = yawQuat;
rec.actor->setGlobalPose(pose);
if (PxRigidDynamic* dyn = rec.actor->is<PxRigidDynamic>()) {
dyn->setRigidDynamicLockFlags(PxRigidDynamicLockFlag::eLOCK_ANGULAR_X | PxRigidDynamicLockFlag::eLOCK_ANGULAR_Z);
}
return true;
#endif
return false;
}
bool PhysicsSystem::getLinearVelocity(int id, glm::vec3& outVelocity) const {
#ifdef MODULARITY_ENABLE_PHYSX
auto it = mActors.find(id);
if (it == mActors.end()) return false;
const ActorRecord& rec = it->second;
if (!rec.actor || !rec.isDynamic) return false;
if (const PxRigidDynamic* dyn = rec.actor->is<PxRigidDynamic>()) {
PxVec3 v = dyn->getLinearVelocity();
outVelocity = glm::vec3(v.x, v.y, v.z);
return true;
}
#endif
return false;
}
bool PhysicsSystem::setActorPose(int id, const glm::vec3& position, const glm::vec3& rotationDeg) {
#ifdef MODULARITY_ENABLE_PHYSX
auto it = mActors.find(id);
if (it == mActors.end()) return false;
ActorRecord& rec = it->second;
if (!rec.actor) return false;
PxTransform pose(ToPxVec3(position), ToPxQuat(rotationDeg));
rec.actor->setGlobalPose(pose);
return true;
#else
(void)id; (void)position; (void)rotationDeg;
return false;
#endif
}
bool PhysicsSystem::raycastClosest(const glm::vec3& origin, const glm::vec3& dir, float distance,
int ignoreId, glm::vec3* hitPos, glm::vec3* hitNormal, float* hitDistance) const {
#ifdef MODULARITY_ENABLE_PHYSX
if (!isReady() || distance <= 0.0f) return false;
PxVec3 unitDir = ToPxVec3(glm::normalize(dir));
if (!unitDir.isFinite()) return false;
PxRaycastBuffer hit;
PxQueryFilterData fd(PxQueryFlag::eSTATIC | PxQueryFlag::eDYNAMIC | PxQueryFlag::ePREFILTER);
IgnoreActorFilter cb(nullptr);
auto it = mActors.find(ignoreId);
if (it != mActors.end()) {
cb.ignore = it->second.actor;
}
bool result = mScene->raycast(ToPxVec3(origin), unitDir, distance, hit,
PxHitFlag::ePOSITION | PxHitFlag::eNORMAL,
fd, cb.ignore ? &cb : nullptr);
if (!result || !hit.hasBlock) return false;
if (hitPos) *hitPos = ToGlmVec3(hit.block.position);
if (hitNormal) *hitNormal = ToGlmVec3(hit.block.normal);
if (hitDistance) *hitDistance = hit.block.distance;
return true;
#else
(void)origin; (void)dir; (void)distance; (void)ignoreId; (void)hitPos; (void)hitNormal; (void)hitDistance;
return false;
#endif
}
void PhysicsSystem::simulate(float deltaTime, std::vector<SceneObject>& objects) {
if (!isReady() || deltaTime <= 0.0f) return;
// Sync actors to authoring transforms before stepping
for (auto& [id, rec] : mActors) {
if (!rec.actor) continue;
auto it = std::find_if(objects.begin(), objects.end(), [id](const SceneObject& o) { return o.id == id; });
if (it == objects.end()) continue;
if (PxRigidDynamic* dyn = rec.actor->is<PxRigidDynamic>()) {
if (dyn->getRigidBodyFlags().isSet(PxRigidBodyFlag::eKINEMATIC)) {
dyn->setKinematicTarget(PxTransform(ToPxVec3(it->position), ToPxQuat(it->rotation)));
}
} else {
// Static actors follow their authoring transform so scripted moves/rotations take effect
rec.actor->setGlobalPose(PxTransform(ToPxVec3(it->position), ToPxQuat(it->rotation)));
}
}
mScene->simulate(deltaTime);
mScene->fetchResults(true);
for (auto& [id, rec] : mActors) {
if (!rec.actor || !rec.isDynamic || rec.isKinematic) continue;
PxTransform pose = rec.actor->getGlobalPose();
auto it = std::find_if(objects.begin(), objects.end(), [id](const SceneObject& o) { return o.id == id; });
if (it == objects.end()) continue;
it->position = ToGlmVec3(pose.p);
it->rotation.y = ToGlmEulerDeg(pose.q).y;
}
}
void PhysicsSystem::shutdown() {
clearActors();
if (mScene) { mScene->release(); mScene = nullptr; }
if (mDispatcher) { mDispatcher->release(); mDispatcher = nullptr; }
if (mPhysics) { mPhysics->release(); mPhysics = nullptr; }
if (mFoundation) { mFoundation->release(); mFoundation = nullptr; }
mDefaultMaterial = nullptr;
}
#else // MODULARITY_ENABLE_PHYSX
bool PhysicsSystem::init() { return false; }
void PhysicsSystem::shutdown() {}
bool PhysicsSystem::isReady() const { return false; }
bool PhysicsSystem::setLinearVelocity(int, const glm::vec3&) { return false; }
bool PhysicsSystem::setActorYaw(int, float) { return false; }
bool PhysicsSystem::getLinearVelocity(int, glm::vec3&) const { return false; }
void PhysicsSystem::onPlayStart(const std::vector<SceneObject>&) {}
void PhysicsSystem::onPlayStop() {}
void PhysicsSystem::simulate(float, std::vector<SceneObject>&) {}
#endif

60
src/PhysicsSystem.h Normal file
View File

@@ -0,0 +1,60 @@
#pragma once
#include "Common.h"
#include "SceneObject.h"
#include <unordered_map>
#include <vector>
#ifdef MODULARITY_ENABLE_PHYSX
#include "PxPhysicsAPI.h"
#include "cooking/PxCooking.h"
#endif
class PhysicsSystem {
public:
bool init();
void shutdown();
bool isReady() const;
bool setLinearVelocity(int id, const glm::vec3& velocity);
bool setActorYaw(int id, float yawDegrees);
bool getLinearVelocity(int id, glm::vec3& outVelocity) const;
bool setActorPose(int id, const glm::vec3& position, const glm::vec3& rotationDeg);
bool raycastClosest(const glm::vec3& origin, const glm::vec3& dir, float distance,
int ignoreId, glm::vec3* hitPos = nullptr,
glm::vec3* hitNormal = nullptr, float* hitDistance = nullptr) const;
void onPlayStart(const std::vector<SceneObject>& objects);
void onPlayStop();
void simulate(float deltaTime, std::vector<SceneObject>& objects);
private:
#ifdef MODULARITY_ENABLE_PHYSX
struct ActorRecord {
physx::PxRigidActor* actor = nullptr;
bool isDynamic = false;
bool isKinematic = false;
};
physx::PxDefaultAllocator mAllocator;
physx::PxDefaultErrorCallback mErrorCallback;
physx::PxFoundation* mFoundation = nullptr;
physx::PxPhysics* mPhysics = nullptr;
physx::PxDefaultCpuDispatcher* mDispatcher = nullptr;
physx::PxScene* mScene = nullptr;
physx::PxMaterial* mDefaultMaterial = nullptr;
physx::PxRigidStatic* mGroundPlane = nullptr;
physx::PxCookingParams mCookParams{physx::PxTolerancesScale()};
std::unordered_map<int, ActorRecord> mActors;
void clearActors();
void createGroundPlane();
ActorRecord createActorFor(const SceneObject& obj) const;
bool attachColliderShape(physx::PxRigidActor* actor, const SceneObject& obj, bool isDynamic) const;
bool attachPrimitiveShape(physx::PxRigidActor* actor, const SceneObject& obj, bool isDynamic) const;
bool gatherMeshData(const SceneObject& obj, std::vector<physx::PxVec3>& vertices, std::vector<uint32_t>& indices) const;
physx::PxTriangleMesh* cookTriangleMesh(const std::vector<physx::PxVec3>& vertices,
const std::vector<uint32_t>& indices) const;
physx::PxConvexMesh* cookConvexMesh(const std::vector<physx::PxVec3>& vertices) const;
#endif
};

View File

@@ -258,7 +258,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
if (!file.is_open()) return false;
file << "# Scene File\n";
file << "version=4\n";
file << "version=7\n";
file << "nextId=" << nextId << "\n";
file << "objectCount=" << objects.size() << "\n";
file << "\n";
@@ -272,6 +272,31 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "position=" << obj.position.x << "," << obj.position.y << "," << obj.position.z << "\n";
file << "rotation=" << obj.rotation.x << "," << obj.rotation.y << "," << obj.rotation.z << "\n";
file << "scale=" << obj.scale.x << "," << obj.scale.y << "," << obj.scale.z << "\n";
file << "hasRigidbody=" << (obj.hasRigidbody ? 1 : 0) << "\n";
if (obj.hasRigidbody) {
file << "rbEnabled=" << (obj.rigidbody.enabled ? 1 : 0) << "\n";
file << "rbMass=" << obj.rigidbody.mass << "\n";
file << "rbUseGravity=" << (obj.rigidbody.useGravity ? 1 : 0) << "\n";
file << "rbKinematic=" << (obj.rigidbody.isKinematic ? 1 : 0) << "\n";
file << "rbLinearDamping=" << obj.rigidbody.linearDamping << "\n";
file << "rbAngularDamping=" << obj.rigidbody.angularDamping << "\n";
}
file << "hasCollider=" << (obj.hasCollider ? 1 : 0) << "\n";
if (obj.hasCollider) {
file << "colliderEnabled=" << (obj.collider.enabled ? 1 : 0) << "\n";
file << "colliderType=" << static_cast<int>(obj.collider.type) << "\n";
file << "colliderBox=" << obj.collider.boxSize.x << "," << obj.collider.boxSize.y << "," << obj.collider.boxSize.z << "\n";
file << "colliderConvex=" << (obj.collider.convex ? 1 : 0) << "\n";
}
file << "hasPlayerController=" << (obj.hasPlayerController ? 1 : 0) << "\n";
if (obj.hasPlayerController) {
file << "pcEnabled=" << (obj.playerController.enabled ? 1 : 0) << "\n";
file << "pcMoveSpeed=" << obj.playerController.moveSpeed << "\n";
file << "pcLookSensitivity=" << obj.playerController.lookSensitivity << "\n";
file << "pcHeight=" << obj.playerController.height << "\n";
file << "pcRadius=" << obj.playerController.radius << "\n";
file << "pcJumpStrength=" << obj.playerController.jumpStrength << "\n";
}
file << "materialColor=" << obj.material.color.r << "," << obj.material.color.g << "," << obj.material.color.b << "\n";
file << "materialAmbient=" << obj.material.ambientStrength << "\n";
file << "materialSpecular=" << obj.material.specularStrength << "\n";
@@ -433,6 +458,47 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
&currentObj->scale.x,
&currentObj->scale.y,
&currentObj->scale.z);
} else if (key == "hasRigidbody") {
currentObj->hasRigidbody = std::stoi(value) != 0;
} else if (key == "rbEnabled") {
currentObj->rigidbody.enabled = std::stoi(value) != 0;
} else if (key == "rbMass") {
currentObj->rigidbody.mass = std::stof(value);
} else if (key == "rbUseGravity") {
currentObj->rigidbody.useGravity = std::stoi(value) != 0;
} else if (key == "rbKinematic") {
currentObj->rigidbody.isKinematic = std::stoi(value) != 0;
} else if (key == "rbLinearDamping") {
currentObj->rigidbody.linearDamping = std::stof(value);
} else if (key == "rbAngularDamping") {
currentObj->rigidbody.angularDamping = std::stof(value);
} else if (key == "hasCollider") {
currentObj->hasCollider = std::stoi(value) != 0;
} else if (key == "colliderEnabled") {
currentObj->collider.enabled = std::stoi(value) != 0;
} else if (key == "colliderType") {
currentObj->collider.type = static_cast<ColliderType>(std::stoi(value));
} else if (key == "colliderBox") {
sscanf(value.c_str(), "%f,%f,%f",
&currentObj->collider.boxSize.x,
&currentObj->collider.boxSize.y,
&currentObj->collider.boxSize.z);
} else if (key == "colliderConvex") {
currentObj->collider.convex = std::stoi(value) != 0;
} else if (key == "hasPlayerController") {
currentObj->hasPlayerController = std::stoi(value) != 0;
} else if (key == "pcEnabled") {
currentObj->playerController.enabled = std::stoi(value) != 0;
} else if (key == "pcMoveSpeed") {
currentObj->playerController.moveSpeed = std::stof(value);
} else if (key == "pcLookSensitivity") {
currentObj->playerController.lookSensitivity = std::stof(value);
} else if (key == "pcHeight") {
currentObj->playerController.height = std::stof(value);
} else if (key == "pcRadius") {
currentObj->playerController.radius = std::stof(value);
} else if (key == "pcJumpStrength") {
currentObj->playerController.jumpStrength = std::stof(value);
} else if (key == "materialColor") {
sscanf(value.c_str(), "%f,%f,%f",
&currentObj->material.color.r,

View File

@@ -98,6 +98,41 @@ struct ScriptComponent {
std::vector<void*> activeIEnums; // function pointers registered via IEnum_Start
};
struct RigidbodyComponent {
bool enabled = true;
float mass = 1.0f;
bool useGravity = true;
bool isKinematic = false;
float linearDamping = 0.05f;
float angularDamping = 0.05f;
};
enum class ColliderType {
Box = 0,
Mesh = 1,
ConvexMesh = 2,
Capsule = 3
};
struct ColliderComponent {
bool enabled = true;
ColliderType type = ColliderType::Box;
glm::vec3 boxSize = glm::vec3(1.0f);
bool convex = true; // For mesh colliders: true = convex hull, false = triangle mesh (static only)
};
struct PlayerControllerComponent {
bool enabled = true;
float moveSpeed = 6.0f;
float lookSensitivity = 0.12f;
float height = 1.8f;
float radius = 0.4f;
float jumpStrength = 6.5f;
float verticalVelocity = 0.0f;
float pitch = 0.0f;
float yaw = 0.0f;
};
class SceneObject {
public:
std::string name;
@@ -124,6 +159,12 @@ public:
PostFXSettings postFx; // Only used when type is PostFXNode
std::vector<ScriptComponent> scripts;
std::vector<std::string> additionalMaterialPaths;
bool hasRigidbody = false;
RigidbodyComponent rigidbody;
bool hasCollider = false;
ColliderComponent collider;
bool hasPlayerController = false;
PlayerControllerComponent playerController;
SceneObject(const std::string& name, ObjectType type, int id)
: name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {}

View File

@@ -39,6 +39,28 @@ void ScriptContext::SetScale(const glm::vec3& scl) {
}
}
bool ScriptContext::HasRigidbody() const {
return object && object->hasRigidbody && object->rigidbody.enabled;
}
bool ScriptContext::SetRigidbodyVelocity(const glm::vec3& velocity) {
if (!engine || !object || !HasRigidbody()) return false;
return engine->setRigidbodyVelocityFromScript(object->id, velocity);
}
bool ScriptContext::GetRigidbodyVelocity(glm::vec3& outVelocity) const {
if (!engine || !object || !HasRigidbody()) return false;
return engine->getRigidbodyVelocityFromScript(object->id, outVelocity);
}
bool ScriptContext::TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rotDeg) {
if (!engine || !object) return false;
object->position = pos;
object->rotation = NormalizeEulerDegrees(rotDeg);
MarkDirty();
return engine->teleportPhysicsActorFromScript(object->id, pos, object->rotation);
}
std::string ScriptContext::GetSetting(const std::string& key, const std::string& fallback) const {
if (!script) return fallback;
auto it = std::find_if(script->settings.begin(), script->settings.end(),

View File

@@ -28,6 +28,10 @@ struct ScriptContext {
void SetPosition(const glm::vec3& pos);
void SetRotation(const glm::vec3& rot);
void SetScale(const glm::vec3& scl);
bool HasRigidbody() const;
bool SetRigidbodyVelocity(const glm::vec3& velocity);
bool GetRigidbodyVelocity(glm::vec3& outVelocity) const;
bool TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rotDeg);
// Settings helpers (auto-mark dirty)
std::string GetSetting(const std::string& key, const std::string& fallback = "") const;
void SetSetting(const std::string& key, const std::string& value);