Yeah! PhysX!!!
This commit is contained in:
@@ -5,6 +5,34 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# ==================== Compiler cache (ccache / sccache) ====================
|
||||
|
||||
option(MODULARITY_USE_COMPILER_CACHE "Enable compiler cache if available" ON)
|
||||
|
||||
if(MODULARITY_USE_COMPILER_CACHE)
|
||||
find_program(CCACHE_PROGRAM ccache)
|
||||
find_program(SCCACHE_PROGRAM sccache)
|
||||
|
||||
if(CCACHE_PROGRAM)
|
||||
message(STATUS "Using compiler cache: ccache (${CCACHE_PROGRAM})")
|
||||
set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
|
||||
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
|
||||
|
||||
elseif(SCCACHE_PROGRAM)
|
||||
message(STATUS "Using compiler cache: sccache (${SCCACHE_PROGRAM})")
|
||||
set(CMAKE_C_COMPILER_LAUNCHER "${SCCACHE_PROGRAM}")
|
||||
set(CMAKE_CXX_COMPILER_LAUNCHER "${SCCACHE_PROGRAM}")
|
||||
|
||||
# Optional (helps some MSVC setups): ensure environment is used consistently
|
||||
# set(ENV{SCCACHE_IDLE_TIMEOUT} "0")
|
||||
|
||||
else()
|
||||
message(STATUS "Compiler cache enabled, but neither ccache nor sccache was found.")
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "Compiler cache disabled (MODULARITY_USE_COMPILER_CACHE=OFF).")
|
||||
endif()
|
||||
|
||||
# ==================== WINDOWS FIXES (only active on Windows) ====================
|
||||
if(WIN32)
|
||||
add_compile_definitions(
|
||||
@@ -16,11 +44,14 @@ endif()
|
||||
|
||||
# ==================== Compiler flags ====================
|
||||
if(MSVC)
|
||||
add_compile_options(/W4 /O2 /permissive- /MP)
|
||||
set(MODULARITY_WARNING_FLAGS /W4 /O2 /permissive- /MP)
|
||||
else()
|
||||
add_compile_options(-Wall -Wextra -Wpedantic -O2)
|
||||
set(MODULARITY_WARNING_FLAGS -Wall -Wextra -Wpedantic -O2)
|
||||
endif()
|
||||
|
||||
# ==================== Optional PhysX ====================
|
||||
option(MODULARITY_ENABLE_PHYSX "Enable PhysX physics integration" ON)
|
||||
|
||||
# ==================== Third-party libraries ====================
|
||||
|
||||
add_subdirectory(src/ThirdParty/glfw EXCLUDE_FROM_ALL)
|
||||
@@ -73,7 +104,9 @@ file(GLOB_RECURSE ENGINE_HEADERS CONFIGURE_DEPENDS
|
||||
)
|
||||
|
||||
list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/ThirdParty/assimp/.*")
|
||||
list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/ThirdParty/PhysX/.*")
|
||||
list(FILTER ENGINE_HEADERS EXCLUDE REGEX ".*/ThirdParty/assimp/.*")
|
||||
list(FILTER ENGINE_HEADERS EXCLUDE REGEX ".*/ThirdParty/PhysX/.*")
|
||||
|
||||
add_library(core STATIC ${ENGINE_SOURCES} ${ENGINE_HEADERS})
|
||||
set(ASSIMP_WARNINGS_AS_ERRORS OFF CACHE BOOL "Disable Assimp warnings as errors" FORCE)
|
||||
@@ -85,10 +118,23 @@ target_include_directories(core PUBLIC
|
||||
${PROJECT_SOURCE_DIR}/src/ThirdParty/assimp/include
|
||||
)
|
||||
target_link_libraries(core PUBLIC glad glm imgui imguizmo)
|
||||
target_compile_options(core PRIVATE ${MODULARITY_WARNING_FLAGS})
|
||||
|
||||
if(MODULARITY_ENABLE_PHYSX)
|
||||
set(PHYSX_ROOT_DIR ${PROJECT_SOURCE_DIR}/src/ThirdParty/PhysX/physx CACHE PATH "PhysX root directory")
|
||||
set(TARGET_BUILD_PLATFORM "linux" CACHE STRING "PhysX build platform (linux/windows)")
|
||||
# PhysX build system expects output locations when using the GameWorks layout.
|
||||
set(PX_OUTPUT_LIB_DIR ${CMAKE_BINARY_DIR}/physx CACHE PATH "PhysX output lib directory")
|
||||
set(PX_OUTPUT_BIN_DIR ${CMAKE_BINARY_DIR}/physx/bin CACHE PATH "PhysX output bin directory")
|
||||
add_subdirectory(${PHYSX_ROOT_DIR}/compiler/public ${CMAKE_BINARY_DIR}/physx)
|
||||
target_include_directories(core PUBLIC ${PHYSX_ROOT_DIR}/include)
|
||||
target_compile_definitions(core PUBLIC MODULARITY_ENABLE_PHYSX PX_PHYSX_STATIC_LIB)
|
||||
target_link_libraries(core PUBLIC PhysX PhysXCommon PhysXFoundation PhysXExtensions PhysXCooking)
|
||||
endif()
|
||||
|
||||
# ==================== Executable ====================
|
||||
add_executable(Modularity src/main.cpp)
|
||||
target_compile_options(Modularity PRIVATE ${MODULARITY_WARNING_FLAGS})
|
||||
|
||||
# Link order matters on Linux
|
||||
if(NOT WIN32)
|
||||
|
||||
@@ -77,6 +77,12 @@ Size=784,221
|
||||
Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
|
||||
[Window][Game Viewport]
|
||||
Pos=306,46
|
||||
Size=1265,737
|
||||
Collapsed=0
|
||||
DockId=0x00000002,1
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,46 Size=1920,960 Split=X
|
||||
DockNode ID=0x00000007 Parent=0xD71539A0 SizeRef=1509,1015 Split=Y
|
||||
|
||||
@@ -127,6 +127,5 @@ void ViewportController::update(GLFWwindow* window, bool& cursorLocked) {
|
||||
viewportFocused = false;
|
||||
manualUnfocus = true;
|
||||
cursorLocked = false;
|
||||
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
212
src/Engine.cpp
212
src/Engine.cpp
@@ -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);
|
||||
|
||||
13
src/Engine.h
13
src/Engine.h
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 we’re 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
529
src/PhysicsSystem.cpp
Normal 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
60
src/PhysicsSystem.h
Normal 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
|
||||
};
|
||||
@@ -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,
|
||||
¤tObj->scale.x,
|
||||
¤tObj->scale.y,
|
||||
¤tObj->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",
|
||||
¤tObj->collider.boxSize.x,
|
||||
¤tObj->collider.boxSize.y,
|
||||
¤tObj->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",
|
||||
¤tObj->material.color.r,
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user