aboutsummaryrefslogtreecommitdiff
path: root/apps/openmb/scene
diff options
context:
space:
mode:
Diffstat (limited to 'apps/openmb/scene')
-rw-r--r--apps/openmb/scene/Camera.cpp203
-rw-r--r--apps/openmb/scene/Camera.hpp69
-rw-r--r--apps/openmb/scene/GridSystem.cpp121
-rw-r--r--apps/openmb/scene/GridSystem.hpp66
-rw-r--r--apps/openmb/scene/VoxelEditor.cpp602
-rw-r--r--apps/openmb/scene/VoxelEditor.hpp132
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