diff options
| author | Ethan Morgan <ethan@gweithio.com> | 2026-02-14 16:44:06 +0000 |
|---|---|---|
| committer | Ethan Morgan <ethan@gweithio.com> | 2026-02-14 16:44:06 +0000 |
| commit | 54409423f767d8b1cf30cb7d0efca6b4ca138823 (patch) | |
| tree | d915ac7828703ce4b963efdd9728a1777ba18c1e /apps/openmb/scene | |
Diffstat (limited to 'apps/openmb/scene')
| -rw-r--r-- | apps/openmb/scene/Camera.cpp | 203 | ||||
| -rw-r--r-- | apps/openmb/scene/Camera.hpp | 69 | ||||
| -rw-r--r-- | apps/openmb/scene/GridSystem.cpp | 121 | ||||
| -rw-r--r-- | apps/openmb/scene/GridSystem.hpp | 66 | ||||
| -rw-r--r-- | apps/openmb/scene/VoxelEditor.cpp | 602 | ||||
| -rw-r--r-- | apps/openmb/scene/VoxelEditor.hpp | 132 |
6 files changed, 1193 insertions, 0 deletions
diff --git a/apps/openmb/scene/Camera.cpp b/apps/openmb/scene/Camera.cpp new file mode 100644 index 0000000..d3a3e7d --- /dev/null +++ b/apps/openmb/scene/Camera.cpp @@ -0,0 +1,203 @@ +#include "Camera.hpp" + +#include <algorithm> +#include <glm/gtc/matrix_transform.hpp> +#include <vector> + +namespace scene { +Camera::Camera () + : mPosition( 0.0f, 0.0f, 3.0f ), mFront( 0.0f, 0.0f, -1.0f ), mWorldUp( 0.0f, 1.0f, 0.0f ), mYaw( -90.0f ), + mPitch( 0.0f ), mMovementSpeed( 3.0f ), mMouseSensitivity( 0.1f ), mFov( 60.0f ), + mVelocity( 0.0f, 0.0f, 0.0f ), mFlying( true ), mGrounded( false ), mSpeedMultiplier( 1.0f ) { + updateCameraVectors(); +} + +Camera::Camera ( const glm::vec3& position, const glm::vec3& up, float yaw, float pitch ) + : mPosition( position ), mWorldUp( up ), mYaw( yaw ), mPitch( pitch ), mMovementSpeed( 3.0f ), + mMouseSensitivity( 0.1f ), mFov( 60.0f ), mVelocity( 0.0f, 0.0f, 0.0f ), mFlying( true ), mGrounded( false ), + mSpeedMultiplier( 1.0f ) { + mFront = glm::vec3( 0.0f, 0.0f, -1.0f ); + updateCameraVectors(); +} + +glm::mat4 Camera::getViewMatrix () const { + return glm::lookAt( mPosition, mPosition + mFront, mUp ); +} + +glm::mat4 Camera::getProjectionMatrix ( float aspect ) const { + return glm::perspective( glm::radians( mFov ), aspect, 0.1f, 100.0f ); +} + +void Camera::processKeyboard ( Movement dir, float deltaTime ) { + float velocity = mMovementSpeed * mSpeedMultiplier * deltaTime; + + glm::vec3 moveForward = mFront; + glm::vec3 moveRight = mRight; + if( !mFlying ) { + moveForward.y = 0.0f; + if( glm::length( moveForward ) < 1e-6f ) + moveForward = glm::vec3( 0.0f, 0.0f, -1.0f ); + moveForward = glm::normalize( moveForward ); + moveRight = glm::normalize( glm::cross( moveForward, mWorldUp ) ); + } + + if( dir == Movement::Forward ) + mPosition += moveForward * velocity; + if( dir == Movement::Backward ) + mPosition -= moveForward * velocity; + if( dir == Movement::Left ) + mPosition -= moveRight * velocity; + if( dir == Movement::Right ) + mPosition += moveRight * velocity; + if( dir == Movement::Up && mFlying ) + mPosition += mWorldUp * velocity; + if( dir == Movement::Down && mFlying ) + mPosition -= mWorldUp * velocity; +} + +void Camera::toggleFly () { + mFlying = !mFlying; + if( mFlying ) { + + mVelocity.y = 0.0f; + } +} + +void Camera::setSpeedMultiplier ( float m ) { + mSpeedMultiplier = m; +} + +static bool aabbOverlap ( const glm::vec3& amin, const glm::vec3& amax, const glm::vec3& bmin, + const glm::vec3& bmax ) { + return ( amin.x <= bmax.x && amax.x >= bmin.x ) && ( amin.y <= bmax.y && amax.y >= bmin.y ) && + ( amin.z <= bmax.z && amax.z >= bmin.z ); +} + +void Camera::updatePhysics ( float deltaTime, const std::vector<std::pair<glm::vec3, glm::vec3>>& worldAABBs, + float floorY ) { + const glm::vec3 halfExtents( 0.3f, 0.9f, 0.3f ); + + if( mFlying ) { + mGrounded = false; + return; + } + + if( !mFlying ) { + const float gravity = -9.8f; + mVelocity.y += gravity * deltaTime; + if( mVelocity.y < -50.0f ) + mVelocity.y = -50.0f; + } + + glm::vec3 newPos = mPosition + mVelocity * deltaTime; + + float camBottom = newPos.y - halfExtents.y; + if( camBottom < floorY ) { + newPos.y = floorY + halfExtents.y; + mVelocity.y = 0.0f; + mGrounded = true; + } else { + mGrounded = false; + } + + for( const auto& box : worldAABBs ) { + glm::vec3 bmin = box.first; + glm::vec3 bmax = box.second; + + glm::vec3 camMin = newPos - halfExtents; + glm::vec3 camMax = newPos + halfExtents; + + if( !aabbOverlap( camMin, camMax, bmin, bmax ) ) + continue; + + float ox = std::min( camMax.x, bmax.x ) - std::max( camMin.x, bmin.x ); + float oy = std::min( camMax.y, bmax.y ) - std::max( camMin.y, bmin.y ); + float oz = std::min( camMax.z, bmax.z ) - std::max( camMin.z, bmin.z ); + + if( ox <= oy && ox <= oz ) { + + float boxCenterX = ( bmin.x + bmax.x ) * 0.5f; + if( newPos.x < boxCenterX ) + newPos.x -= ox; + else + newPos.x += ox; + } else if( oy <= ox && oy <= oz ) { + float boxCenterY = ( bmin.y + bmax.y ) * 0.5f; + if( newPos.y < boxCenterY ) { + newPos.y -= oy; + mVelocity.y = 0.0f; + } else { + newPos.y += oy; + mVelocity.y = 0.0f; + mGrounded = true; + } + } else { + float boxCenterZ = ( bmin.z + bmax.z ) * 0.5f; + if( newPos.z < boxCenterZ ) + newPos.z -= oz; + else + newPos.z += oz; + } + } + + mPosition = newPos; +} + +void Camera::jump () { + if( mFlying ) + return; + if( mGrounded ) { + const float jumpImpulse = 5.0f; + mVelocity.y = jumpImpulse; + mGrounded = false; + } +} + +bool Camera::isFlying () const { + return mFlying; +} + +bool Camera::isGrounded () const { + return mGrounded; +} + +float Camera::getSpeedMultiplier () const { + return mSpeedMultiplier; +} + +void Camera::processMouseMovement ( float xoffset, float yoffset, bool constrainPitch ) { + xoffset *= mMouseSensitivity; + yoffset *= mMouseSensitivity; + + mYaw += xoffset; + mPitch += yoffset; + + if( constrainPitch ) { + if( mPitch > 89.0f ) + mPitch = 89.0f; + if( mPitch < -89.0f ) + mPitch = -89.0f; + } + + updateCameraVectors(); +} + +void Camera::processMouseScroll ( float yoffset ) { + mFov -= yoffset; + if( mFov < 1.0f ) + mFov = 1.0f; + if( mFov > 90.0f ) + mFov = 90.0f; +} + +void Camera::updateCameraVectors () { + glm::vec3 front; + front.x = cos( glm::radians( mYaw ) ) * cos( glm::radians( mPitch ) ); + front.y = sin( glm::radians( mPitch ) ); + front.z = sin( glm::radians( mYaw ) ) * cos( glm::radians( mPitch ) ); + mFront = glm::normalize( front ); + mRight = glm::normalize( glm::cross( mFront, mWorldUp ) ); + mUp = glm::normalize( glm::cross( mRight, mFront ) ); +} + +} // namespace scene diff --git a/apps/openmb/scene/Camera.hpp b/apps/openmb/scene/Camera.hpp new file mode 100644 index 0000000..7c5edc7 --- /dev/null +++ b/apps/openmb/scene/Camera.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include <glm/glm.hpp> +#include <utility> +#include <vector> + +namespace scene { +enum class Movement { + Forward, + Backward, + Left, + Right, + Up, + Down +}; + +class Camera { + public: + Camera(); + Camera( const glm::vec3& position, const glm::vec3& up, float yaw, float pitch ); + + glm::mat4 getViewMatrix() const; + glm::mat4 getProjectionMatrix( float aspect ) const; + + void processKeyboard( Movement dir, float deltaTime ); + void processMouseMovement( float xoffset, float yoffset, bool constrainPitch = true ); + void processMouseScroll( float yoffset ); + + void toggleFly(); + void setSpeedMultiplier( float m ); + void jump(); + + bool isFlying() const; + bool isGrounded() const; + float getSpeedMultiplier() const; + + void updatePhysics( float deltaTime, const std::vector<std::pair<glm::vec3, glm::vec3>>& worldAABBs, + float floorY = 0.0f ); + + glm::vec3 position () const { return mPosition; } + + float getPitch () const { return mPitch; } + + float getYaw () const { return mYaw; } + + private: + void updateCameraVectors(); + + private: + glm::vec3 mPosition; + glm::vec3 mFront; + glm::vec3 mUp; + glm::vec3 mRight; + glm::vec3 mWorldUp; + + float mYaw; + float mPitch; + + float mMovementSpeed; + float mMouseSensitivity; + float mFov; + + glm::vec3 mVelocity; + bool mFlying; + bool mGrounded; + float mSpeedMultiplier; +}; + +} // namespace scene diff --git a/apps/openmb/scene/GridSystem.cpp b/apps/openmb/scene/GridSystem.cpp new file mode 100644 index 0000000..273cb02 --- /dev/null +++ b/apps/openmb/scene/GridSystem.cpp @@ -0,0 +1,121 @@ +#include "GridSystem.hpp" + +#include <cmath> + +namespace scene { +GridSystem::GridSystem () + : mCellSize( 1.0f ), mFloorY( 0.0f ), mGridWidth( 1000 ), mGridDepth( 1000 ), mWallHeight( 4 ) { +} + +float GridSystem::getCellSize () const { + return mCellSize; +} + +float GridSystem::getFloorY () const { + return mFloorY; +} + +int GridSystem::getGridWidth () const { + return mGridWidth; +} + +int GridSystem::getGridDepth () const { + return mGridDepth; +} + +int GridSystem::getWallHeight () const { + return mWallHeight; +} + +glm::vec3 GridSystem::gridToWorld ( int gridX, int gridZ, float y ) const { + float halfW = mGridWidth * 0.5f; + float halfD = mGridDepth * 0.5f; + + float worldX = ( gridX - halfW ) * mCellSize + mCellSize * 0.5f; + float worldZ = ( gridZ - halfD ) * mCellSize + mCellSize * 0.5f; + + return glm::vec3( worldX, y, worldZ ); +} + +glm::vec3 GridSystem::gridToWorldFloor ( int gridX, int gridZ ) const { + return gridToWorld( gridX, gridZ, mFloorY ); +} + +bool GridSystem::worldToGrid ( const glm::vec3& worldPos, int& outGridX, int& outGridZ ) const { + float halfW = mGridWidth * 0.5f; + float halfD = mGridDepth * 0.5f; + + float localX = worldPos.x / mCellSize + halfW - 0.5f; + float localZ = worldPos.z / mCellSize + halfD - 0.5f; + + outGridX = static_cast<int>( std::floor( localX ) ); + outGridZ = static_cast<int>( std::floor( localZ ) ); + + return ( outGridX >= 0 && outGridX < mGridWidth && outGridZ >= 0 && outGridZ < mGridDepth ); +} + +glm::vec3 GridSystem::getCellCenter ( int gridX, int gridZ, int cellY ) const { + float y = mFloorY + cellY * mCellSize + mCellSize * 0.5f; + return gridToWorld( gridX, gridZ, y ); +} + +float GridSystem::getMinWorldX () const { + float halfW = mGridWidth * 0.5f; + return ( 0 - halfW ) * mCellSize; +} + +float GridSystem::getMaxWorldX () const { + float halfW = mGridWidth * 0.5f; + return ( mGridWidth - 1 - halfW ) * mCellSize + mCellSize; +} + +float GridSystem::getMinWorldZ () const { + float halfD = mGridDepth * 0.5f; + return ( 0 - halfD ) * mCellSize; +} + +float GridSystem::getMaxWorldZ () const { + float halfD = mGridDepth * 0.5f; + return ( mGridDepth - 1 - halfD ) * mCellSize + mCellSize; +} + +float GridSystem::getHalfWidth () const { + return mGridWidth * 0.5f; +} + +float GridSystem::getHalfDepth () const { + return mGridDepth * 0.5f; +} + +float GridSystem::getFrontWallZ () const { + float halfD = mGridDepth * 0.5f; + return ( 0 - halfD ) * mCellSize + mCellSize * 0.5f; +} + +float GridSystem::getBackWallZ () const { + float halfD = mGridDepth * 0.5f; + return ( mGridDepth - 1 - halfD ) * mCellSize + mCellSize * 0.5f; +} + +float GridSystem::getLeftWallX () const { + float halfW = mGridWidth * 0.5f; + return ( 0 - halfW ) * mCellSize + mCellSize * 0.5f; +} + +float GridSystem::getRightWallX () const { + float halfW = mGridWidth * 0.5f; + return ( mGridWidth - 1 - halfW ) * mCellSize + mCellSize * 0.5f; +} + +float GridSystem::getWallMinY () const { + return mFloorY; +} + +float GridSystem::getWallMaxY () const { + return mFloorY + mWallHeight * mCellSize; +} + +float GridSystem::getWallBaseY () const { + return mFloorY + mCellSize; +} +} // namespace scene diff --git a/apps/openmb/scene/GridSystem.hpp b/apps/openmb/scene/GridSystem.hpp new file mode 100644 index 0000000..7fff5d2 --- /dev/null +++ b/apps/openmb/scene/GridSystem.hpp @@ -0,0 +1,66 @@ +#ifndef OPENMB_APPS_OPENMB_SCENE_GRIDSYSTEM_H +#define OPENMB_APPS_OPENMB_SCENE_GRIDSYSTEM_H + +#include <glm/glm.hpp> + +namespace scene { + +class GridSystem { + public: + GridSystem(); + ~GridSystem() = default; + + float getCellSize() const; + + float getFloorY() const; + + int getGridWidth() const; + + int getGridDepth() const; + + int getWallHeight() const; + + glm::vec3 gridToWorld( int gridX, int gridZ, float y ) const; + + glm::vec3 gridToWorldFloor( int gridX, int gridZ ) const; + + bool worldToGrid( const glm::vec3& worldPos, int& outGridX, int& outGridZ ) const; + + glm::vec3 getCellCenter( int gridX, int gridZ, int cellY ) const; + + float getMinWorldX() const; + + float getMaxWorldX() const; + + float getMinWorldZ() const; + + float getMaxWorldZ() const; + + float getHalfWidth() const; + + float getHalfDepth() const; + + float getFrontWallZ() const; + + float getBackWallZ() const; + + float getLeftWallX() const; + + float getRightWallX() const; + + float getWallMinY() const; + + float getWallMaxY() const; + + float getWallBaseY() const; + + private: + float mCellSize; + float mFloorY; + int mGridWidth; + int mGridDepth; + int mWallHeight; +}; +} // namespace scene + +#endif diff --git a/apps/openmb/scene/VoxelEditor.cpp b/apps/openmb/scene/VoxelEditor.cpp new file mode 100644 index 0000000..b113580 --- /dev/null +++ b/apps/openmb/scene/VoxelEditor.cpp @@ -0,0 +1,602 @@ +#include "VoxelEditor.hpp" + +#include "GridSystem.hpp" + +#include <algorithm> +#include <cmath> +#include <fstream> + +#include <nlohmann/json.hpp> + +namespace scene { +VoxelEditor::VoxelEditor ( const GridSystem& gridSystem ) + : mGridSystem( gridSystem ), mPlacedSet(), mPlacedList(), mCellTextureIds(), mCellCollidable(), mUndoStack(), + mRedoStack(), mPlacedModels(), mDragging( false ), mDragButton( -1 ), mDragStartCell( 0 ), mPreviewCells(), + mPrevLeftDown( false ), mPrevRightDown( false ) { +} + +void VoxelEditor::addModelInstance ( const ModelInstance& mi ) { + + Action action; + Action::ModelInstance ami; + ami.modelIndex = mi.modelIndex; + ami.pos = mi.pos; + ami.yaw = mi.yaw; + ami.scale = mi.scale; + ami.mCollidable = mi.mCollidable; + action.mAddedModels.push_back( ami ); + mPlacedModels.push_back( mi ); + + mUndoStack.push_back( action ); + if( mUndoStack.size() > mMaxUndoSteps ) + mUndoStack.erase( mUndoStack.begin() ); + mRedoStack.clear(); +} + +const std::vector<VoxelEditor::ModelInstance>& VoxelEditor::getPlacedModels () const { + return mPlacedModels; +} + +void VoxelEditor::processInput ( const glm::vec3& rayOrigin, const glm::vec3& rayDir, bool leftMouseDown, + bool rightMouseDown, bool shiftPressed, + const std::vector<std::pair<glm::vec3, glm::vec3>>& baseWorldBoxes, + int currentTextureId, bool placeCollidable ) { + bool hitExisting = false; + float bestT = FLT_MAX; + glm::ivec3 hitCell( 0 ); + glm::vec3 hitNormal( 0.0f ); + + for( const auto& c : mPlacedList ) { + auto aabb = cellToAABB( c ); + float t = 0.0f; + if( rayAABBIntersect( rayOrigin, rayDir, aabb.first, aabb.second, t ) ) { + if( t >= 0.0f && t < bestT ) { + bestT = t; + hitExisting = true; + hitCell = c; + glm::vec3 hitPoint = rayOrigin + rayDir * t; + glm::vec3 center = ( aabb.first + aabb.second ) * 0.5f; + glm::vec3 diff = hitPoint - center; + glm::vec3 ad = glm::abs( diff ); + if( ad.x > ad.y && ad.x > ad.z ) + hitNormal = glm::vec3( diff.x > 0.0f ? 1.0f : -1.0f, 0.0f, 0.0f ); + else if( ad.y > ad.x && ad.y > ad.z ) + hitNormal = glm::vec3( 0.0f, diff.y > 0.0f ? 1.0f : -1.0f, 0.0f ); + else + hitNormal = glm::vec3( 0.0f, 0.0f, diff.z > 0.0f ? 1.0f : -1.0f ); + } + } + } + + glm::ivec3 placeCell( 0 ); + bool validHit = false; + + if( !hitExisting ) { + float wallT = FLT_MAX; + glm::vec3 wallHitPoint( 0.0f ); + glm::vec3 wallHitNormal( 0.0f ); + bool hitWall = false; + + for( const auto& box : baseWorldBoxes ) { + float t = 0.0f; + if( rayAABBIntersect( rayOrigin, rayDir, box.first, box.second, t ) ) { + if( t > 0.0f && t < wallT ) { + wallT = t; + wallHitPoint = rayOrigin + rayDir * t; + glm::vec3 center = ( box.first + box.second ) * 0.5f; + glm::vec3 diff = wallHitPoint - center; + glm::vec3 ad = glm::abs( diff ); + if( ad.x > ad.y && ad.x > ad.z ) + wallHitNormal = glm::vec3( diff.x > 0.0f ? 1.0f : -1.0f, 0.0f, 0.0f ); + else if( ad.y > ad.x && ad.y > ad.z ) + wallHitNormal = glm::vec3( 0.0f, diff.y > 0.0f ? 1.0f : -1.0f, 0.0f ); + else + wallHitNormal = glm::vec3( 0.0f, 0.0f, diff.z > 0.0f ? 1.0f : -1.0f ); + hitWall = true; + } + } + } + + if( hitWall ) { + + glm::vec3 offsetPoint = wallHitPoint + wallHitNormal * 0.01f; + + int xi = 0, zi = 0; + mGridSystem.worldToGrid( offsetPoint, xi, zi ); + int yi = static_cast<int>( std::floor( offsetPoint.y / mGridSystem.getCellSize() ) ); + + hitCell = glm::ivec3( xi, yi, zi ); + hitNormal = wallHitNormal; + placeCell = hitCell; + validHit = true; + } else if( std::abs( rayDir.y ) > 1e-6f ) { + float t = ( 0.0f - rayOrigin.y ) / rayDir.y; + if( t > 0.0f ) { + glm::vec3 hitPoint = rayOrigin + rayDir * t; + hitCell = worldPosToCell( hitPoint ); + hitNormal = glm::vec3( 0.0f, 1.0f, 0.0f ); + placeCell = hitCell; + validHit = true; + } + } + } + + if( hitExisting ) { + validHit = true; + placeCell = hitCell + glm::ivec3( static_cast<int>( std::round( hitNormal.x ) ), + static_cast<int>( std::round( hitNormal.y ) ), + static_cast<int>( std::round( hitNormal.z ) ) ); + placeCell.x = std::clamp( placeCell.x, 0, mGridSystem.getGridWidth() - 1 ); + placeCell.z = std::clamp( placeCell.z, 0, mGridSystem.getGridDepth() - 1 ); + } else if( validHit ) { + placeCell.x = std::clamp( placeCell.x, 0, mGridSystem.getGridWidth() - 1 ); + placeCell.z = std::clamp( placeCell.z, 0, mGridSystem.getGridDepth() - 1 ); + } + + if( leftMouseDown && !mPrevLeftDown ) { + mDragging = true; + mDragButton = 0; + mDragStartCell = placeCell; + } + if( rightMouseDown && !mPrevRightDown ) { + mDragging = true; + mDragButton = 1; + mDragStartCell = placeCell; + } + + if( mDragging ) { + glm::ivec3 endCell = placeCell; + if( shiftPressed ) { + endCell.y = mDragStartCell.y; + } + mPreviewCells = rasterizeGridBox( mDragStartCell, endCell ); + } + + if( !leftMouseDown && mPrevLeftDown && mDragButton == 0 ) { + Action action; + for( const auto& cell : mPreviewCells ) { + if( mPlacedSet.insert( cell ).second ) { + mPlacedList.push_back( cell ); + mCellTextureIds[cell] = currentTextureId; + mCellCollidable[cell] = placeCollidable; + VoxelData voxelData; + voxelData.mCell = cell; + voxelData.mTextureId = currentTextureId; + voxelData.mCollidable = placeCollidable; + action.mAddedCells.push_back( voxelData ); + } + } + if( !action.mAddedCells.empty() ) { + mUndoStack.push_back( action ); + if( mUndoStack.size() > mMaxUndoSteps ) + mUndoStack.erase( mUndoStack.begin() ); + mRedoStack.clear(); + } + mDragging = false; + } + + if( !rightMouseDown && mPrevRightDown && mDragButton == 1 ) { + Action action; + for( const auto& cell : mPreviewCells ) { + if( mPlacedSet.erase( cell ) > 0 ) { + mPlacedList.erase( std::remove( mPlacedList.begin(), mPlacedList.end(), cell ), mPlacedList.end() ); + auto texIt = mCellTextureIds.find( cell ); + VoxelData voxelData; + voxelData.mCell = cell; + voxelData.mTextureId = ( texIt != mCellTextureIds.end() ) ? texIt->second : 0; + auto collIt = mCellCollidable.find( cell ); + voxelData.mCollidable = ( collIt != mCellCollidable.end() ) ? collIt->second : true; + action.mRemovedCells.push_back( voxelData ); + mCellTextureIds.erase( cell ); + mCellCollidable.erase( cell ); + } + } + if( !action.mRemovedCells.empty() ) { + mUndoStack.push_back( action ); + if( mUndoStack.size() > mMaxUndoSteps ) + mUndoStack.erase( mUndoStack.begin() ); + mRedoStack.clear(); + } + mDragging = false; + } + + if( !mDragging ) { + if( validHit ) { + mPreviewCells.clear(); + mPreviewCells.push_back( placeCell ); + } else { + mPreviewCells.clear(); + } + } + + mPrevLeftDown = leftMouseDown; + mPrevRightDown = rightMouseDown; +} + +void VoxelEditor::undo () { + if( mUndoStack.empty() ) + return; + + Action action = mUndoStack.back(); + mUndoStack.pop_back(); + + for( const auto& voxelData : action.mAddedCells ) { + mPlacedSet.erase( voxelData.mCell ); + mPlacedList.erase( std::remove( mPlacedList.begin(), mPlacedList.end(), voxelData.mCell ), + mPlacedList.end() ); + mCellTextureIds.erase( voxelData.mCell ); + mCellCollidable.erase( voxelData.mCell ); + } + for( const auto& voxelData : action.mRemovedCells ) { + if( mPlacedSet.insert( voxelData.mCell ).second ) { + mPlacedList.push_back( voxelData.mCell ); + mCellTextureIds[voxelData.mCell] = voxelData.mTextureId; + mCellCollidable[voxelData.mCell] = voxelData.mCollidable; + } + } + + for( const auto& mi : action.mAddedModels ) { + + auto it = std::find_if( mPlacedModels.begin(), mPlacedModels.end(), + [&] ( const ModelInstance& m ) { + return m.modelIndex == mi.modelIndex && m.pos == mi.pos && m.yaw == mi.yaw && + m.scale == mi.scale; + } ); + if( it != mPlacedModels.end() ) + mPlacedModels.erase( it ); + } + for( const auto& mi : action.mRemovedModels ) { + ModelInstance m; + m.modelIndex = mi.modelIndex; + m.pos = mi.pos; + m.yaw = mi.yaw; + m.scale = mi.scale; + m.mCollidable = mi.mCollidable; + mPlacedModels.push_back( m ); + } + + mRedoStack.push_back( action ); +} + +void VoxelEditor::redo () { + if( mRedoStack.empty() ) + return; + + Action action = mRedoStack.back(); + mRedoStack.pop_back(); + + for( const auto& voxelData : action.mAddedCells ) { + if( mPlacedSet.insert( voxelData.mCell ).second ) { + mPlacedList.push_back( voxelData.mCell ); + mCellTextureIds[voxelData.mCell] = voxelData.mTextureId; + mCellCollidable[voxelData.mCell] = voxelData.mCollidable; + } + } + for( const auto& voxelData : action.mRemovedCells ) { + mPlacedSet.erase( voxelData.mCell ); + mPlacedList.erase( std::remove( mPlacedList.begin(), mPlacedList.end(), voxelData.mCell ), + mPlacedList.end() ); + mCellTextureIds.erase( voxelData.mCell ); + mCellCollidable.erase( voxelData.mCell ); + } + + for( const auto& mi : action.mAddedModels ) { + ModelInstance m; + m.modelIndex = mi.modelIndex; + m.pos = mi.pos; + m.yaw = mi.yaw; + m.scale = mi.scale; + m.mCollidable = mi.mCollidable; + mPlacedModels.push_back( m ); + } + for( const auto& mi : action.mRemovedModels ) { + auto it = std::find_if( mPlacedModels.begin(), mPlacedModels.end(), + [&] ( const ModelInstance& m ) { + return m.modelIndex == mi.modelIndex && m.pos == mi.pos && m.yaw == mi.yaw && + m.scale == mi.scale; + } ); + if( it != mPlacedModels.end() ) + mPlacedModels.erase( it ); + } + + mUndoStack.push_back( action ); +} + +const std::vector<glm::ivec3>& VoxelEditor::getPlacedCells () const { + return mPlacedList; +} + +const std::vector<glm::ivec3>& VoxelEditor::getPreviewCells () const { + return mPreviewCells; +} + +size_t VoxelEditor::getUndoStackSize () const { + return mUndoStack.size(); +} + +size_t VoxelEditor::getRedoStackSize () const { + return mRedoStack.size(); +} + +glm::ivec3 VoxelEditor::worldPosToCell ( const glm::vec3& pos ) const { + int xi = 0, zi = 0; + mGridSystem.worldToGrid( pos, xi, zi ); + int yi = static_cast<int>( std::floor( ( pos.y - mGridSystem.getFloorY() ) / mGridSystem.getCellSize() ) ); + return glm::ivec3( xi, yi, zi ); +} + +glm::vec3 VoxelEditor::cellToWorldCenter ( const glm::ivec3& cell ) const { + return mGridSystem.getCellCenter( cell.x, cell.z, cell.y ); +} + +std::pair<glm::vec3, glm::vec3> VoxelEditor::cellToAABB ( const glm::ivec3& cell ) const { + glm::vec3 center = cellToWorldCenter( cell ); + float halfSize = mGridSystem.getCellSize() * 0.5f; + glm::vec3 he( halfSize, halfSize, halfSize ); + return { center - he, center + he }; +} + +std::vector<std::pair<glm::vec3, glm::vec3>> +VoxelEditor::getAllCollisionBoxes ( const std::vector<std::pair<glm::vec3, glm::vec3>>& baseWorldBoxes ) const { + std::vector<std::pair<glm::vec3, glm::vec3>> boxes = baseWorldBoxes; + for( const auto& c : mPlacedList ) { + auto it = mCellCollidable.find( c ); + bool coll = true; + if( it != mCellCollidable.end() ) + coll = it->second; + if( coll ) + boxes.push_back( cellToAABB( c ) ); + } + + return boxes; +} + +bool VoxelEditor::rayAABBIntersect ( const glm::vec3& ro, const glm::vec3& rd, const glm::vec3& bmin, + const glm::vec3& bmax, float& outT ) const { + float tmin = -FLT_MAX; + float tmax = FLT_MAX; + for( int i = 0; i < 3; ++i ) { + float origin = ro[i]; + float dir = rd[i]; + float minB = bmin[i]; + float maxB = bmax[i]; + if( std::abs( dir ) < 1e-6f ) { + if( origin < minB || origin > maxB ) + return false; + } else { + float t1 = ( minB - origin ) / dir; + float t2 = ( maxB - origin ) / dir; + if( t1 > t2 ) + std::swap( t1, t2 ); + tmin = std::max( tmin, t1 ); + tmax = std::min( tmax, t2 ); + if( tmin > tmax ) + return false; + } + } + if( tmax < 0.0f ) + return false; + outT = tmin >= 0.0f ? tmin : tmax; + return true; +} + +std::vector<glm::ivec3> VoxelEditor::rasterizeGridBox ( const glm::ivec3& a, const glm::ivec3& b ) const { + std::vector<glm::ivec3> out; + glm::ivec3 minC( std::min( a.x, b.x ), std::min( a.y, b.y ), std::min( a.z, b.z ) ); + glm::ivec3 maxC( std::max( a.x, b.x ), std::max( a.y, b.y ), std::max( a.z, b.z ) ); + + minC.x = std::clamp( minC.x, 0, mGridSystem.getGridWidth() - 1 ); + maxC.x = std::clamp( maxC.x, 0, mGridSystem.getGridWidth() - 1 ); + minC.z = std::clamp( minC.z, 0, mGridSystem.getGridDepth() - 1 ); + maxC.z = std::clamp( maxC.z, 0, mGridSystem.getGridDepth() - 1 ); + + for( int x = minC.x; x <= maxC.x; ++x ) { + for( int y = minC.y; y <= maxC.y; ++y ) { + for( int z = minC.z; z <= maxC.z; ++z ) { + out.emplace_back( x, y, z ); + } + } + } + + return out; +} + +std::vector<glm::ivec3> VoxelEditor::rasterizeCircle ( const glm::ivec3& centerCell, int radiusCells, + int heightLayers ) const { + std::vector<glm::ivec3> out; + + if( radiusCells < 1 ) + radiusCells = 1; + if( heightLayers < 1 ) + heightLayers = 1; + + int minX = std::max( 0, centerCell.x - radiusCells ); + int maxX = std::min( mGridSystem.getGridWidth() - 1, centerCell.x + radiusCells ); + int minZ = std::max( 0, centerCell.z - radiusCells ); + int maxZ = std::min( mGridSystem.getGridDepth() - 1, centerCell.z + radiusCells ); + + int maxY = mGridSystem.getWallHeight() - 1; + + int r2 = radiusCells * radiusCells; + + for( int x = minX; x <= maxX; ++x ) { + for( int z = minZ; z <= maxZ; ++z ) { + int dx = x - centerCell.x; + int dz = z - centerCell.z; + if( dx * dx + dz * dz <= r2 ) { + for( int h = 0; h < heightLayers; ++h ) { + int y = centerCell.y + h; + y = std::clamp( y, 0, maxY ); + out.emplace_back( x, y, z ); + } + } + } + } + + return out; +} + +void VoxelEditor::applyCircularBrush ( const glm::vec3& centerWorld, float radiusWorld, float /*heightWorld*/, + int textureId, bool placeCollidable ) { + float cellSize = mGridSystem.getCellSize(); + + int radiusCells = std::max( 1, static_cast<int>( std::ceil( radiusWorld / cellSize ) ) ); + + glm::ivec3 centerCell = worldPosToCell( centerWorld ); + + int minX = std::max( 0, centerCell.x - radiusCells ); + int maxX = std::min( mGridSystem.getGridWidth() - 1, centerCell.x + radiusCells ); + int minZ = std::max( 0, centerCell.z - radiusCells ); + int maxZ = std::min( mGridSystem.getGridDepth() - 1, centerCell.z + radiusCells ); + + int targetY = centerCell.y; + int maxY = mGridSystem.getWallHeight() - 1; + targetY = std::clamp( targetY, 0, maxY ); + + float r2 = radiusWorld * radiusWorld; + + Action action; + + for( int x = minX; x <= maxX; ++x ) { + for( int z = minZ; z <= maxZ; ++z ) { + + glm::vec3 cellCenter = mGridSystem.getCellCenter( x, z, targetY ); + float dx = cellCenter.x - centerWorld.x; + float dz = cellCenter.z - centerWorld.z; + if( dx * dx + dz * dz <= r2 ) { + glm::ivec3 cell( x, targetY, z ); + if( mPlacedSet.insert( cell ).second ) { + mPlacedList.push_back( cell ); + mCellTextureIds[cell] = textureId; + mCellCollidable[cell] = placeCollidable; + VoxelData vd; + vd.mCell = cell; + vd.mTextureId = textureId; + vd.mCollidable = placeCollidable; + action.mAddedCells.push_back( vd ); + } + } + } + } + + if( !action.mAddedCells.empty() ) { + mUndoStack.push_back( action ); + if( mUndoStack.size() > mMaxUndoSteps ) + mUndoStack.erase( mUndoStack.begin() ); + mRedoStack.clear(); + } +} + +int VoxelEditor::getTextureIdForCell ( const glm::ivec3& cell ) const { + auto it = mCellTextureIds.find( cell ); + if( it != mCellTextureIds.end() ) + return it->second; + return -1; +} + +bool VoxelEditor::saveToFile ( const std::string& filepath ) const { + try { + nlohmann::json j; + j["version"] = 1; + + nlohmann::json voxelsArray = nlohmann::json::array(); + for( const auto& cell : mPlacedList ) { + auto it = mCellTextureIds.find( cell ); + int textureId = ( it != mCellTextureIds.end() ) ? it->second : 0; + + nlohmann::json voxel; + voxel["x"] = cell.x; + voxel["y"] = cell.y; + voxel["z"] = cell.z; + voxel["textureId"] = textureId; + auto cit = mCellCollidable.find( cell ); + voxel["collidable"] = ( cit != mCellCollidable.end() ) ? cit->second : true; + voxelsArray.push_back( voxel ); + } + + j["voxels"] = voxelsArray; + + nlohmann::json modelsArray = nlohmann::json::array(); + for( const auto& m : mPlacedModels ) { + nlohmann::json mj; + mj["modelIndex"] = m.modelIndex; + mj["x"] = m.pos.x; + mj["y"] = m.pos.y; + mj["z"] = m.pos.z; + mj["yaw"] = m.yaw; + mj["scale"] = m.scale; + mj["collidable"] = m.mCollidable; + modelsArray.push_back( mj ); + } + j["models"] = modelsArray; + + std::ofstream file( filepath ); + if( !file.is_open() ) + return false; + + file << j.dump( 2 ); + file.close(); + return true; + } catch( ... ) { + return false; + } +} + +bool VoxelEditor::loadFromFile ( const std::string& filepath ) { + try { + std::ifstream file( filepath ); + if( !file.is_open() ) + return false; + + nlohmann::json j; + file >> j; + file.close(); + + if( !j.contains( "version" ) || !j.contains( "voxels" ) ) + return false; + + clear(); + + for( const auto& voxelJson : j["voxels"] ) { + glm::ivec3 cell( voxelJson["x"].get<int>(), voxelJson["y"].get<int>(), + voxelJson["z"].get<int>() ); + + int textureId = voxelJson.value( "textureId", 0 ); + bool collidable = voxelJson.value( "collidable", true ); + + if( mPlacedSet.insert( cell ).second ) { + mPlacedList.push_back( cell ); + mCellTextureIds[cell] = textureId; + mCellCollidable[cell] = collidable; + } + } + + if( j.contains( "models" ) && j["models"].is_array() ) { + for( const auto& mj : j["models"] ) { + ModelInstance mi; + mi.modelIndex = mj.value( "modelIndex", 0 ); + mi.pos.x = mj.value( "x", 0.0f ); + mi.pos.y = mj.value( "y", 0.0f ); + mi.pos.z = mj.value( "z", 0.0f ); + mi.yaw = mj.value( "yaw", 0.0f ); + mi.scale = mj.value( "scale", 1.0f ); + mi.mCollidable = mj.value( "collidable", true ); + mPlacedModels.push_back( mi ); + } + } + + return true; + } catch( ... ) { + return false; + } +} + +void VoxelEditor::clear () { + mPlacedSet.clear(); + mPlacedList.clear(); + mCellTextureIds.clear(); + mUndoStack.clear(); + mRedoStack.clear(); + mPlacedModels.clear(); +} +} // namespace scene diff --git a/apps/openmb/scene/VoxelEditor.hpp b/apps/openmb/scene/VoxelEditor.hpp new file mode 100644 index 0000000..a77f547 --- /dev/null +++ b/apps/openmb/scene/VoxelEditor.hpp @@ -0,0 +1,132 @@ +#ifndef OPENMB_APPS_OPENMB_SCENE_VOXELEDITOR_H +#define OPENMB_APPS_OPENMB_SCENE_VOXELEDITOR_H + +#include <glm/glm.hpp> +#include <string> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +namespace scene { +class GridSystem; + +struct IVec3Hash { + size_t operator()( const glm::ivec3& v ) const noexcept { + uint32_t x = static_cast<uint32_t>( v.x ); + uint32_t y = static_cast<uint32_t>( v.y ); + uint32_t z = static_cast<uint32_t>( v.z ); + return (size_t)( ( x * 73856093u ) ^ ( y * 19349663u ) ^ ( z * 83492791u ) ); + } +}; + +struct IVec3Eq { + bool operator()( const glm::ivec3& a, const glm::ivec3& b ) const noexcept { + return a.x == b.x && a.y == b.y && a.z == b.z; + } +}; + +class VoxelEditor { + public: + VoxelEditor( const GridSystem& gridSystem ); + ~VoxelEditor() = default; + + void processInput( const glm::vec3& rayOrigin, const glm::vec3& rayDir, bool leftMouseDown, bool rightMouseDown, + bool shiftPressed, const std::vector<std::pair<glm::vec3, glm::vec3>>& baseWorldBoxes, + int currentTextureId, bool placeCollidable ); + + void applyCircularBrush( const glm::vec3& centerWorld, float radiusWorld, float heightWorld, int textureId, + bool placeCollidable ); + + void undo(); + + void redo(); + + const std::vector<glm::ivec3>& getPlacedCells() const; + + const std::vector<glm::ivec3>& getPreviewCells() const; + + size_t getUndoStackSize() const; + + size_t getRedoStackSize() const; + + glm::ivec3 worldPosToCell( const glm::vec3& pos ) const; + + glm::vec3 cellToWorldCenter( const glm::ivec3& cell ) const; + + std::pair<glm::vec3, glm::vec3> cellToAABB( const glm::ivec3& cell ) const; + + std::vector<std::pair<glm::vec3, glm::vec3>> + getAllCollisionBoxes( const std::vector<std::pair<glm::vec3, glm::vec3>>& baseWorldBoxes ) const; + + int getTextureIdForCell( const glm::ivec3& cell ) const; + + bool saveToFile( const std::string& filepath ) const; + + bool loadFromFile( const std::string& filepath ); + + void clear(); + + struct ModelInstance { + int modelIndex; + glm::vec3 pos; + float yaw; + float scale; + bool mCollidable; + }; + + void addModelInstance( const ModelInstance& mi ); + const std::vector<ModelInstance>& getPlacedModels() const; + + private: + struct VoxelData { + glm::ivec3 mCell; + int mTextureId; + bool mCollidable; + }; + + struct Action { + std::vector<VoxelData> mAddedCells; + std::vector<VoxelData> mRemovedCells; + struct ModelInstance { + int modelIndex; + glm::vec3 pos; + float yaw; + float scale; + bool mCollidable; + }; + std::vector<ModelInstance> mAddedModels; + std::vector<ModelInstance> mRemovedModels; + }; + + bool rayAABBIntersect( const glm::vec3& ro, const glm::vec3& rd, const glm::vec3& bmin, const glm::vec3& bmax, + float& outT ) const; + + std::vector<glm::ivec3> rasterizeGridBox( const glm::ivec3& a, const glm::ivec3& b ) const; + + std::vector<glm::ivec3> rasterizeCircle( const glm::ivec3& centerCell, int radiusCells, + int heightLayers ) const; + + const GridSystem& mGridSystem; + + std::unordered_set<glm::ivec3, IVec3Hash, IVec3Eq> mPlacedSet; + std::vector<glm::ivec3> mPlacedList; + std::unordered_map<glm::ivec3, int, IVec3Hash, IVec3Eq> mCellTextureIds; + std::unordered_map<glm::ivec3, bool, IVec3Hash, IVec3Eq> mCellCollidable; + + std::vector<Action> mUndoStack; + std::vector<Action> mRedoStack; + static constexpr int mMaxUndoSteps = 100; + + std::vector<ModelInstance> mPlacedModels; + + bool mDragging; + int mDragButton; + glm::ivec3 mDragStartCell; + std::vector<glm::ivec3> mPreviewCells; + + bool mPrevLeftDown; + bool mPrevRightDown; +}; +} // namespace scene + +#endif |