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/VoxelEditor.cpp | |
Diffstat (limited to 'apps/openmb/scene/VoxelEditor.cpp')
| -rw-r--r-- | apps/openmb/scene/VoxelEditor.cpp | 602 |
1 files changed, 602 insertions, 0 deletions
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 |