/* Copyright 2003-2005 ROBLOX Corporation, All Rights Reserved */ #include "stdafx.h" #include "V8DataModel/MegaCluster.h" #include "G3D/CollisionDetection.h" #include "Reflection/Reflection.h" #include "util/Region3int16.h" #include "util/RunStateOwner.h" #include "util/stringbuffer.h" #include "V8DataModel/Workspace.h" #include "V8World/Primitive.h" #include "V8World/MegaClusterPoly.h" #include "V8World/SmoothClusterGeometry.h" #include "V8World/TerrainPartition.h" #include "V8World/World.h" #include "V8World/ContactManager.h" #include "Voxel/CellChangeListener.h" #include "V8DataModel/TerrainRegion.h" #include "Network/Players.h" #include "v8datamodel/ChangeHistory.h" #include "Voxel2/Grid.h" #include "Voxel2/MaterialTable.h" #include "Voxel2/Mesher.h" #include "Voxel2/Conversion.h" #include "script/LuaArguments.h" LOGVARIABLE(TerrainCellListener, 0) FASTINTVARIABLE(SmoothTerrainMaxLuaRegion, 4*1024*1024) FASTINTVARIABLE(SmoothTerrainMaxCppRegion, 64*1024*1024) namespace RBX { using namespace Voxel; const Vector3int16 kLegacyChunkCount(16, 4, 16); const Vector3int16 kLegacyChunkOffset(8, 0, 8); static const ContentId kSmoothTerrainMaterials("rbxasset://terrain/materials.json"); namespace Reflection { template<> Reflection::EnumDesc::EnumDesc() :RBX::Reflection::EnumDescriptor("CellMaterial") { addPair(CELL_MATERIAL_Deprecated_Empty, "Empty"); addPair(CELL_MATERIAL_Grass, "Grass"); addPair(CELL_MATERIAL_Sand, "Sand"); addPair(CELL_MATERIAL_Brick, "Brick"); addPair(CELL_MATERIAL_Granite, "Granite"); addPair(CELL_MATERIAL_Asphalt, "Asphalt"); addPair(CELL_MATERIAL_Iron, "Iron"); addPair(CELL_MATERIAL_Aluminum, "Aluminum"); addPair(CELL_MATERIAL_Gold, "Gold"); addPair(CELL_MATERIAL_Wood_Plank, "WoodPlank"); addPair(CELL_MATERIAL_Wood_Log, "WoodLog"); addPair(CELL_MATERIAL_Gravel, "Gravel"); addPair(CELL_MATERIAL_Cinder_Block, "CinderBlock"); addPair(CELL_MATERIAL_Stone_Block, "MossyStone"); addPair(CELL_MATERIAL_Cement, "Cement"); addPair(CELL_MATERIAL_Red_Plastic, "RedPlastic"); addPair(CELL_MATERIAL_Blue_Plastic, "BluePlastic"); addPair(CELL_MATERIAL_Water, "Water"); } template<> CellMaterial& Variant::convert(void) { return genericConvert(); } template<> Reflection::EnumDesc::EnumDesc() :RBX::Reflection::EnumDescriptor("CellBlock") { addPair(CELL_BLOCK_Solid, "Solid" ); addPair(CELL_BLOCK_VerticalWedge, "VerticalWedge" ); addPair(CELL_BLOCK_CornerWedge, "CornerWedge" ); addPair(CELL_BLOCK_InverseCornerWedge, "InverseCornerWedge" ); addPair(CELL_BLOCK_HorizontalWedge, "HorizontalWedge" ); } template<> CellBlock& Variant::convert(void) { return genericConvert(); } template<> Reflection::EnumDesc::EnumDesc() :RBX::Reflection::EnumDescriptor("CellOrientation") { addPair(CELL_ORIENTATION_NegZ, "NegZ"); addPair(CELL_ORIENTATION_X, "X"); addPair(CELL_ORIENTATION_Z, "Z"); addPair(CELL_ORIENTATION_NegX, "NegX"); } template<> CellOrientation& Variant::convert(void) { return genericConvert(); } template<> Reflection::EnumDesc::EnumDesc() :RBX::Reflection::EnumDescriptor("WaterForce") { addPair(WATER_CELL_FORCE_None, "None"); addPair(WATER_CELL_FORCE_Small, "Small"); addPair(WATER_CELL_FORCE_Medium, "Medium"); addPair(WATER_CELL_FORCE_Strong, "Strong"); addPair(WATER_CELL_FORCE_MaxForce, "Max"); } template<> WaterCellForce& Variant::convert(void) { return genericConvert(); } template<> Reflection::EnumDesc::EnumDesc() :RBX::Reflection::EnumDescriptor("WaterDirection") { addPair(WATER_CELL_DIRECTION_NegX, "NegX"); addPair(WATER_CELL_DIRECTION_X, "X"); addPair(WATER_CELL_DIRECTION_NegY, "NegY"); addPair(WATER_CELL_DIRECTION_Y, "Y"); addPair(WATER_CELL_DIRECTION_NegZ, "NegZ"); addPair(WATER_CELL_DIRECTION_Z, "Z"); } template<> WaterCellDirection& Variant::convert(void) { return genericConvert(); } } template<> bool RBX::StringConverter::convertToValue(const std::string& text, CellMaterial& value) { return Reflection::EnumDesc::singleton().convertToValue(text.c_str(),value); } template<> bool RBX::StringConverter::convertToValue(const std::string& text, CellBlock& value) { return Reflection::EnumDesc::singleton().convertToValue(text.c_str(),value); } template<> bool RBX::StringConverter::convertToValue(const std::string& text, CellOrientation& value) { return Reflection::EnumDesc::singleton().convertToValue(text.c_str(),value); } template<> bool RBX::StringConverter::convertToValue(const std::string& text, WaterCellForce& value) { return Reflection::EnumDesc::singleton().convertToValue(text.c_str(),value); } template<> bool RBX::StringConverter::convertToValue(const std::string& text, WaterCellDirection& value) { return Reflection::EnumDesc::singleton().convertToValue(text.c_str(),value); } // these defaults should all correspond with char cell = 0 const CellMaterial MegaClusterInstance::kDefaultMaterial = CELL_MATERIAL_Deprecated_Empty; const CellBlock MegaClusterInstance::kDefaultBlock = CELL_BLOCK_Solid; const CellOrientation MegaClusterInstance::kDefaultOrientation = CELL_ORIENTATION_NegZ; const char* const sMegaCluster = "Terrain"; using namespace Reflection; REFLECTION_BEGIN(); // LUA property reflections static Reflection::PropDescriptor desc_ClusterGridV1("ClusterGrid", category_Data, NULL, &MegaClusterInstance::setPackagedClusterGridV1, Reflection::PropertyDescriptor::LEGACY, Security::None); static Reflection::PropDescriptor desc_ClusterGridV2("ClusterGridV2", category_Data, NULL, &MegaClusterInstance::setPackagedClusterGridV2, Reflection::PropertyDescriptor::LEGACY, Security::None); static Reflection::PropDescriptor desc_ClusterGridV3("ClusterGridV3", category_Data, &MegaClusterInstance::getPackagedClusteredGridV3, &MegaClusterInstance::setPackagedClusterGridV3, Reflection::PropertyDescriptor::CLUSTER, Security::None); static Reflection::PropDescriptor desc_SmoothGrid("SmoothGrid", category_Data, &MegaClusterInstance::getPackagedSmoothGrid, &MegaClusterInstance::setPackagedSmoothGrid, Reflection::PropertyDescriptor::CLUSTER, Security::None); static Reflection::PropDescriptor desc_SmoothReplicate("SmoothReplicate", category_Data, &MegaClusterInstance::getSmoothReplicate, &MegaClusterInstance::setSmoothReplicate, Reflection::PropertyDescriptor::REPLICATE_ONLY, Security::None); static Reflection::PropDescriptor prop_MaxExtents("MaxExtents", category_Data, &MegaClusterInstance::getMaxExtents, NULL, PropertyDescriptor::UI ); // LUA function reflections static Reflection::BoundFuncDesc(int,int,int)> func_getCell(&MegaClusterInstance::getCellScript, "GetCell", "x", "y", "z", Security::None); static Reflection::BoundFuncDesc func_setCell(&MegaClusterInstance::setCellScript, "SetCell", "x", "y", "z", "material", "block", "orientation", Security::None ); static Reflection::BoundFuncDesc func_setCells(&MegaClusterInstance::setCellsScript, "SetCells", "region", "material", "block", "orientation", Security::None ); static Reflection::BoundFuncDesc(int,int,int)> func_getWaterCell(&MegaClusterInstance::getWaterCellScript,"GetWaterCell", "x", "y", "z", Security::None); static Reflection::BoundFuncDesc func_setWaterCell(&MegaClusterInstance::setWaterCellScript, "SetWaterCell", "x", "y", "z", "force", "direction", Security::None ); static Reflection::BoundFuncDesc func_autoWedgeCell(&MegaClusterInstance::autoWedgeCellScript, "AutowedgeCell", "x", "y", "z", Security::None ); static Reflection::BoundFuncDesc func_autoWedgeCells(&MegaClusterInstance::autoWedgeCellsScript, "AutowedgeCells", "region", Security::None ); static Reflection::BoundFuncDesc func_cellCornerToWorld(&MegaClusterInstance::cellCornerToWorldScript, "CellCornerToWorld", "x", "y", "z", Security::None ); static Reflection::BoundFuncDesc func_cellCenterToWorld(&MegaClusterInstance::cellCenterToWorldScript, "CellCenterToWorld", "x", "y", "z", Security::None ); static Reflection::BoundFuncDesc func_worldToCellPreferSolid(&MegaClusterInstance::worldToCellPreferSolidScript, "WorldToCellPreferSolid", "position", Security::None ); static Reflection::BoundFuncDesc func_worldToCellPreferEmpty(&MegaClusterInstance::worldToCellPreferEmptyScript, "WorldToCellPreferEmpty", "position", Security::None ); static Reflection::BoundFuncDesc func_worldToCell(&MegaClusterInstance::worldToCellScript, "WorldToCell", "position", Security::None ); static Reflection::BoundFuncDesc func_clear(&MegaClusterInstance::clear, "Clear", Security::None ); static Reflection::BoundFuncDesc func_countCells(&MegaClusterInstance::countCellsScript, "CountCells", Security::None ); static Reflection::BoundFuncDesc (Region3int16)> func_copyRegion(&MegaClusterInstance::copyRegion, "CopyRegion", "region", Security::None ); static Reflection::BoundFuncDesc, Vector3int16, bool)> func_pasteRegion(&MegaClusterInstance::pasteRegion, "PasteRegion", "region", "corner", "pasteEmptyCells", Security::None ); static Reflection::PropDescriptor prop_IsSmooth("IsSmooth", category_Data, &MegaClusterInstance::isSmooth, NULL, Reflection::PropertyDescriptor::UI, Security::None); static Reflection::BoundFuncDesc func_convertToSmooth(&MegaClusterInstance::convertToSmooth, "ConvertToSmooth", Security::Plugin); static Reflection::CustomBoundFuncDesc(Region3, float)> func_readVoxels(&MegaClusterInstance::readVoxels, "ReadVoxels", "region", "resolution", Security::None ); static Reflection::CustomBoundFuncDesc, shared_ptr)> func_writeVoxels(&MegaClusterInstance::writeVoxels, "WriteVoxels", "region", "resolution", "materials", "occupancy", Security::None ); static Reflection::BoundFuncDesc func_fillRegion(&MegaClusterInstance::fillRegion, "FillRegion", "region", "resolution", "material", Security::None ); static Reflection::BoundFuncDesc func_fillBlock(&MegaClusterInstance::fillBlock, "FillBlock", "cframe", "size", "material", Security::None ); static Reflection::BoundFuncDesc func_fillBall(&MegaClusterInstance::fillBall, "FillBall", "center", "radius", "material", Security::None ); REFLECTION_END(); static Reflection::PropDescriptor desc_WaterColor("WaterColor", category_Appearance, &MegaClusterInstance::getWaterColor, &MegaClusterInstance::setWaterColor); static Reflection::PropDescriptor desc_WaterTransparency("WaterTransparency", category_Appearance, &MegaClusterInstance::getWaterTransparency, &MegaClusterInstance::setWaterTransparency); static Reflection::PropDescriptor desc_WaterWaveSize("WaterWaveSize", category_Appearance, &MegaClusterInstance::getWaterWaveSize, &MegaClusterInstance::setWaterWaveSize); static Reflection::PropDescriptor desc_WaterWaveSpeed("WaterWaveSpeed", category_Appearance, &MegaClusterInstance::getWaterWaveSpeed, &MegaClusterInstance::setWaterWaveSpeed); // Properties to make read-only with constant return values const bool MegaClusterInstance::kConstArchivable = true; const BrickColor MegaClusterInstance::kConstBrickColor = BrickColor(); const bool MegaClusterInstance::kConstCanCollide = true; const float MegaClusterInstance::kConstElasticity(0.3f); const float MegaClusterInstance::kConstFriction(0.5f); const bool MegaClusterInstance::kConstLocked = true; const PartMaterial MegaClusterInstance::kConstMaterial(PLASTIC_MATERIAL); const PhysicalProperties MegaClusterInstance::kConstCustomPhysicalProperties = PhysicalProperties(); const std::string MegaClusterInstance::kConstName("Terrain"); const float MegaClusterInstance::kConstReflectance = 0; const Vector3 MegaClusterInstance::kConstRotVelocity(0, 0, 0); const float MegaClusterInstance::kConstTransparency = 0; const Vector3 MegaClusterInstance::kConstVelocity(0, 0, 0); // Member methods void MegaClusterInstance::destroyJoints() { // Cannot throw as long as this extends PartInstance because other // property updates trigger destroyJoints(). } shared_ptr MegaClusterInstance::luaClone() { throw std::runtime_error("Cannot Clone() Terrain"); } void MegaClusterInstance::destroy() { throw std::runtime_error("Cannot Destroy() Terrain"); } void MegaClusterInstance::join() { // Cannot throw as long as this extends PartInstance because other // property updates trigger join(). } bool MegaClusterInstance::resize(NormalId normalId, int deltaAmount) { throw std::runtime_error("Cannot Resize() Terrain"); } shared_ptr MegaClusterInstance::getTouchingParts() { throw RBX::runtime_error("GetTouchingParts is not a valid member of Terrain"); } MegaClusterInstance::MegaClusterInstance() : smoothReplicate(-1) , waterColor(0.05f, 0.33f, 0.36f) , waterTransparency(0.3f) , waterWaveSize(0.15f) , waterWaveSpeed(10.f) { FASTLOG1(FLog::MegaClusterInit, "MegaCluster created - %p", this); Vector3 terrainSize = Vector3(511*4,63*4,511*4); setName( "Terrain" ); PartInstance::setPartSizeXml(terrainSize); setAnchored(true); setPartLocked(true); setElasticity(kConstElasticity); setFriction(kConstFriction); // Initialize tables for legacy terrain initBlockOrientationFaceMap(); // Initialize tables for smooth terrain materialTable.reset(new Voxel2::MaterialTable(ContentProvider::findAsset(kSmoothTerrainMaterials), Voxel2::Cell::Material_Max + 1)); Voxel2::Mesher::prepareTables(); } MegaClusterInstance::~MegaClusterInstance() { FASTLOG1(FLog::MegaClusterInit, "MegaCluster destroyed - %p", this); Workspace* parentWorkspace = dynamic_cast( this->getParent() ); if( parentWorkspace ) parentWorkspace->setTerrain( NULL ); // This makes sure we delete physics geometry before voxel grid getPartPrimitive()->setGeometryType(Geometry::GEOMETRY_BLOCK); } void MegaClusterInstance::setAnchored(bool value) { // Mega Clusters are ALWAYS anchored Super::setAnchored( true ); } void MegaClusterInstance::setPartSizeXml(const Vector3& rbxSize) { // do nothing. this property should be essentially read only } void MegaClusterInstance::setPartSizeUi(const Vector3& rbxSize) { // do nothing. this property should be essentially read only } void MegaClusterInstance::setTranslationUi(const Vector3& set) { // do nothing. this property should be essentially read only } void MegaClusterInstance::setCoordinateFrame(const CoordinateFrame& value) { // Disregard any input coordinate frame. Instead, send a fixed cluster // coordinate frame PartInstance::setCoordinateFrame(clusterCoordinateFrame); } void MegaClusterInstance::verifySetParent(const Instance* instance) const { Super::verifySetParent(instance); const Workspace* workspace = instance != NULL ? instance->fastDynamicCast() : NULL; if( instance != NULL && ((workspace == NULL) || (workspace->getTerrain() != NULL)) ){ throw RBX::runtime_error("Unable to change Terrain's parent. Workspace already has Terrain"); } if (instance) const_cast(this)->initialize(); } void MegaClusterInstance::initialize() { if (!voxelGrid && !smoothGrid) { if (smoothReplicate == 1) initializeGridSmooth(); // terrain replicated from server, smooth else if (smoothReplicate == 0) initializeGridMega(); // terrain replicated from server, mega else initializeGridMega(); // default terrain, mega (for now!) } } PartType MegaClusterInstance::getPartType() const { return MEGACLUSTER_PART; //return BLOCK_PART; } bool MegaClusterInstance::isAllocated() const { if (smoothGrid) return smoothGrid->isAllocated(); if (voxelGrid) return voxelGrid->isAllocated(); return false; } void MegaClusterInstance::onServiceProvider(ServiceProvider* oldProvider, ServiceProvider* newProvider) { Super::onServiceProvider(oldProvider, newProvider); FASTLOG2(FLog::MegaClusterInit, "onServiceProvider, oldProvider: %p, new parent: %p", oldProvider, newProvider); if(newProvider == NULL && oldProvider != NULL) { Workspace* workspace = Workspace::findWorkspace(oldProvider); if(workspace->getTerrain() == this) { FASTLOG(FLog::MegaClusterInit, "onServiceProvider: Clearing terrain instance"); workspace->setTerrain(NULL); } } } void MegaClusterInstance::onAncestorChanged(const AncestorChanged& event) { Super::onAncestorChanged(event); FASTLOG1(FLog::MegaClusterInit, "Ancestor change, new parent: %p", event.newParent); if(event.newParent == NULL) { return; } Workspace* workspace = Workspace::findWorkspace(event.newParent); if(!workspace->getTerrain()) { FASTLOG(FLog::MegaClusterInit, "No terrain yet - it's going go be me!"); workspace->setTerrain(this); } else if( workspace->getTerrain() != this ) { FASTLOG(FLog::MegaClusterInit, "Terrain already exists - replace the existing one with this one"); workspace->setTerrain(this); } } shared_ptr MegaClusterInstance::getCellScript(int x, int y, int z) { validateApi(); if (smoothGrid) { using namespace Voxel2::Conversion; Voxel2::Cell cell = smoothGrid->getCell(x, y, z); shared_ptr result(new Reflection::Tuple(3)); if (cell.getMaterial() == Voxel2::Cell::Material_Water) { result->values[0] = CELL_MATERIAL_Water; result->values[1] = kDefaultBlock; result->values[2] = kDefaultOrientation; } else { CellBlock block = getCellBlockFromCell(cell); if (block == CELL_BLOCK_Empty) { result->values[0] = kDefaultMaterial; result->values[1] = kDefaultBlock; result->values[2] = kDefaultOrientation; } else { result->values[0] = getCellMaterialFromMaterial(getMaterialFromVoxelMaterial(cell.getMaterial())); result->values[1] = block; result->values[2] = kDefaultOrientation; } } return result; } else { Vector3int16 internalPos = Vector3int16(x,y,z); Cell cellValue = voxelGrid->getCell(internalPos); Cell waterCell = voxelGrid->getWaterCell(internalPos); CellMaterial cellMaterial = (CellMaterial) voxelGrid->getCellMaterial(internalPos); shared_ptr result(new Reflection::Tuple(3)); if ( cellValue.solid.getBlock() == CELL_BLOCK_Empty ) { if (!waterCell.isEmpty()) { result->values[0] = CELL_MATERIAL_Water; } else { result->values[0] = kDefaultMaterial; } result->values[1] = kDefaultBlock; result->values[2] = kDefaultOrientation; } else { result->values[0] = cellMaterial; result->values[1] = cellValue.solid.getBlock(); result->values[2] = cellValue.solid.getOrientation(); } return result; } } void MegaClusterInstance::setCellsScript(Region3int16 region, CellMaterial material, CellBlock block, CellOrientation orientation ) { validateApi(); if (smoothGrid) { Vector3int16 minPos = region.getMinPos(); Vector3int16 maxPos = region.getMaxPos(); if (minPos.x <= maxPos.x && minPos.y <= maxPos.y && minPos.z <= maxPos.z) { Voxel2::Region rr(Vector3int32(minPos), Vector3int32(maxPos) + Vector3int32::one()); Vector3int32 rsize = rr.size(); Voxel2::Box box(rsize.x, rsize.y, rsize.z); using namespace Voxel2::Conversion; Voxel2::Cell cell = (material == CELL_MATERIAL_Water) ? Voxel2::Cell(Voxel2::Cell::Material_Water, Voxel2::Cell::Occupancy_Max) : (material == CELL_MATERIAL_Deprecated_Empty || block == CELL_BLOCK_Empty) ? Voxel2::Cell() : Voxel2::Cell(getVoxelMaterialFromMaterial(getMaterialFromCellMaterial(material)), getOccupancyFromSolidBlock(block)); if (cell.getMaterial() != Voxel2::Cell::Material_Air) { for (int y = 0; y < rsize.y; ++y) for (int z = 0; z < rsize.z; ++z) { Voxel2::Cell* row = box.writeRow(0, y, z); for (int x = 0; x < rsize.x; ++x) row[x] = cell; } } smoothGrid->write(rr, box); } } else { Vector3int16 minPos = region.getMinPos(); Vector3int16 maxPos = region.getMaxPos(); // Data is stored in (Y > Z > X) format [so Y is most significant index and X is least in pointer arithmetic], so traverse in this order for significant speed-up! for( int j = minPos.y; j <= maxPos.y; ++j ) { for( int k = minPos.z; k <= maxPos.z; ++k ) { for( int i = minPos.x; i <= maxPos.x; ++i ) { setCellScript(i, j, k, material, block, orientation); } } } } } bool MegaClusterInstance::autoWedgeCellScript(int scriptX, int scriptY, int scriptZ) { validateApi(); if (smoothGrid) return false; Vector3int16 internalCellPos = Vector3int16(scriptX, scriptY, scriptZ); Cell currentCell = voxelGrid->getCell(internalCellPos); CellMaterial cellMat = voxelGrid->getCellMaterial(internalCellPos); if (currentCell.solid.getBlock() != CELL_BLOCK_Empty) { Cell cell; bool surroundings[3][2][3]; bool sides[4]; bool corners[4]; bool sidesAbove[4]; bool cornersAbove[4]; int adjacentSides = 0; int adjacentSidesAbove = 0; int adjacentCorners = 0; int adjacentCornersAbove = 0; bool blockBelow = (voxelGrid->getCell(Vector3int16(internalCellPos.x, internalCellPos.y - 1, internalCellPos.z)).solid.getBlock() != CELL_BLOCK_Empty); // build out the surrounding geometry for ( int j = 0; j <= 1; ++j ){ for ( int k = 0; k <= 2; ++k ){ for ( int i = 0; i <= 2; ++i ){ surroundings[i][j][k] = (voxelGrid->getCell(Vector3int16(internalCellPos.x - 1 + i, internalCellPos.y + j, internalCellPos.z - 1 + k)).solid.getBlock() != CELL_BLOCK_Empty); } } } for ( int step = 0; step <= 2; step += 2 ){ sides[step] = surroundings[step][0][1]; sides[step + 1] = surroundings[1][0][2 - step]; sidesAbove[step] = surroundings[step][1][1]; sidesAbove[step + 1] = surroundings[1][1][2 - step]; corners[step] = surroundings[step][0][step]; corners[step + 1] = surroundings[step][0][2 - step]; cornersAbove[step] = surroundings[step][1][step]; cornersAbove[step + 1] = surroundings[step][1][2 - step]; } for ( int i = 0; i < 4; ++i ){ if (sides[i]) adjacentSides++; if (corners[i]) adjacentCorners++; if (sidesAbove[i]) adjacentSidesAbove++; if (cornersAbove[i]) adjacentCornersAbove++; } // Figure out which cell to autowedge to: CellBlock wedge; CellOrientation rotation; // type 1: 45 degree ramp [must not have a block on top and be surrounded by 1 side; or 3 sides and the 2 corners between them] if (!surroundings[1][1][1]) { if (adjacentSides == 1 && blockBelow) // if only surrounded by 1 side, then MUST have a block beneath it { for (int n = 0; n < 4; ++n ) { if (sides[n]) { wedge = (CellBlock)1; rotation = (CellOrientation)((n + 1) % 4); cell.solid.setBlock(wedge); cell.solid.setOrientation(rotation); voxelGrid->setCell(internalCellPos, cell, cellMat); return true; } } } else if (adjacentSides == 3) { for (int n = 0; n < 4; ++n ) { if (sides[n] && corners[(n + 1) % 4] && sides[(n + 1) % 4] && corners[(n + 2) % 4] && sides[(n + 2) % 4]) { wedge = (CellBlock)1; rotation = (CellOrientation)((n + 2) % 4); cell.solid.setBlock(wedge); cell.solid.setOrientation(rotation); voxelGrid->setCell(internalCellPos, cell, cellMat); return true; } } } //type 2: 45 degree corner [must not have a block on top, and be surrounded by 2 sides and the 1 corner between them; or 3 sides and 1 corner between 2 of them (facing towards that corner)] for (int n = 0; n < 4; ++n ) { if (sides[n] && corners[(n + 1) % 4] && sides[(n + 1) % 4] && (adjacentSides == 2 || (adjacentSides == 3 && (corners[(n + 3) % 4] || (sides[(n + 2) % 4] && corners[(n + 2) % 4]) || (sides[(n + 3) % 4] && corners[n]))))) { wedge = (CellBlock)2; rotation = (CellOrientation)((n + 2) % 4); cell.solid.setBlock(wedge); cell.solid.setOrientation(rotation); voxelGrid->setCell(internalCellPos, cell, cellMat); return true; } } } //type 3: 45 degree inverse corner [surrounded by three sides or 4 sides and 3 corners, with nothing above or else a block on top surrounded on 2 sides and the corner between them] if (adjacentSides == 3 && surroundings[1][1][1]) { if (adjacentCorners > 1) { for ( int n = 0; n < 4; ++n ) { if ((!corners[n] || !cornersAbove[n]) && (!sides[(n - 1) % 4] || !sides[n]) && (!sidesAbove[n] && sidesAbove[(n + 1) % 4] && sidesAbove[(n + 2) % 4] && !sidesAbove[(n + 3) % 4])) { wedge = (CellBlock)3; rotation = (CellOrientation)((n + 3) % 4); cell.solid.setBlock(wedge); cell.solid.setOrientation(rotation); voxelGrid->setCell(internalCellPos, cell, cellMat); return true; } } } } else if ( adjacentSides == 4 && adjacentCorners == 3 ) { for ( int n = 0; n < 4; ++n ) { if (!corners[n] && (!surroundings[1][1][1] || (!sidesAbove[n] && sidesAbove[(n + 1) % 4] && cornersAbove[(n + 2) % 4] && sidesAbove[(n + 2) % 4] && !sidesAbove[(n + 3) % 4]))) { wedge = (CellBlock)3; rotation = (CellOrientation)((n + 3) % 4); cell.solid.setBlock(wedge); cell.solid.setOrientation(rotation); voxelGrid->setCell(internalCellPos, cell, cellMat); return true; } } } //type 4: half a cube, as if it were cut diagonally from front to back [surrounded by 2 sides] if (adjacentSides == 2 && adjacentCorners < 4) { for ( int n = 0; n < 4; ++n ) { if ( !sides[n] && !sides[(n + 1) % 4] && (!surroundings[1][1][1] || (!sidesAbove[n] && !sidesAbove[(n + 1) % 4] && sidesAbove[(n + 2) % 4] && sidesAbove[(n + 3) % 4]))) { wedge = (CellBlock)4; rotation = (CellOrientation)n; cell.solid.setBlock(wedge); cell.solid.setOrientation(rotation); voxelGrid->setCell(internalCellPos, cell, cellMat); return true; } } } // should be a regular block wedge = (CellBlock)0; rotation = (CellOrientation)0; cell.solid.setBlock(wedge); cell.solid.setOrientation(rotation); voxelGrid->setCell(internalCellPos, cell, cellMat); return false; } // was empty return false; } void MegaClusterInstance::autoWedgeCellsScript(Region3int16 region) { validateApi(); if (smoothGrid) return; std::vector chunks = voxelGrid->getNonEmptyChunksInRegion(region); for (size_t i = 0; i < chunks.size(); ++i) { Region3int16 regionExtents = SpatialRegion::inclusiveVoxelExtentsOfRegion(chunks[i]); Vector3int16 minPos = regionExtents.getMinPos().max(region.getMinPos()); Vector3int16 maxPos = regionExtents.getMaxPos().min(region.getMaxPos()); for (int y = minPos.y; y <= maxPos.y; ++y) for (int z = minPos.z; z <= maxPos.z; ++z) for (int x = minPos.x; x <= maxPos.x; ++x) autoWedgeCellScript(x, y, z); } } void MegaClusterInstance::autoWedgeCellsInternal(Region3int16 region) { Vector3int16 minPos = region.getMinPos(); Vector3int16 maxPos = region.getMaxPos(); autoWedgeCellsScript(Region3int16(minPos, maxPos)); } Surface MegaClusterInstance::getSurface(const RbxRay& gridRay, int& surfaceId) { Geometry* geometry = getPrimitive(this)->getGeometry(); Vector3 notUsed1; CoordinateFrame notUsed2; if (geometry->hitTestTerrain(gridRay, notUsed1, surfaceId, notUsed2)) return Surface(this, NormalId(surfaceId)); else return Surface(); } template void writeCountValue(Stream& s, unsigned count) { RBXASSERT(count <= 0xffff); if(count < 255) { s << ((unsigned char)count); } else { s << (unsigned char)255; s << (unsigned char)(count >> 8); s << (unsigned char)(count & 0xff); } } template unsigned readCountValue(Stream& s) { unsigned char count; s >> count; if(count < 255) { return count; } else { unsigned char hi, lo; s >> hi; s >> lo; return (hi << 8) | lo; } } template void writeInt16(Stream& s, short value) { unsigned char v0 = value; unsigned char v1 = value >> 8; s << v0; s << v1; } template short readInt16(Stream& s) { unsigned char v0, v1; s >> v0; s >> v1; return v0 | (v1 << 8); } template void MegaClusterInstance::decodeChunkDataFromStreamV1_Deprecated(const Vector3int16& chunkPos, Stream& encodedData) { Vector3int16 cellInChunkIndex = Vector3int16( 0, 0, 0 ); while( cellInChunkIndex.y < Voxel::kY_CHUNK_SIZE ) { unsigned char value; unsigned count; encodedData >> value; count = readCountValue(encodedData); RBXASSERT(count > 0); for( unsigned i = 0; i < count; ++i ) { Vector3int16 cellPos = Vector3int16( chunkPos.x * Voxel::kXZ_CHUNK_SIZE, chunkPos.y * Voxel::kY_CHUNK_SIZE, chunkPos.z * Voxel::kXZ_CHUNK_SIZE ); cellPos += cellInChunkIndex; setCellInternalV1_Deprecated( cellPos, value ); incrementCellInChunkIndex( cellInChunkIndex ); // validate we're not trying to process into other chunks if (cellInChunkIndex.y >= Voxel::kY_CHUNK_SIZE) return; } } } template void MegaClusterInstance::decodeChunkDataFromStream(Voxel::Grid& grid, const Vector3int16& chunkPos, Stream& encodedData) { FASTLOG3(FLog::MegaClusterDecodeStream, "Decode chunk pos (%d,%d,%d)", chunkPos.x, chunkPos.y, chunkPos.z); std::vector cells(Voxel::kXZ_CHUNK_SIZE * Voxel::kXZ_CHUNK_SIZE * Voxel::kY_CHUNK_SIZE); std::vector materials(Voxel::kXZ_CHUNK_SIZE * Voxel::kXZ_CHUNK_SIZE * Voxel::kY_CHUNK_SIZE); // Read cell data unsigned int totalCells = 0; while (totalCells < cells.size()) { unsigned char value; unsigned count; encodedData >> value; count = std::min(readCountValue(encodedData), static_cast(cells.size() - totalCells)); RBXASSERT(count > 0); memset(&cells[totalCells], value, count); totalCells += count; } // Read material data unsigned int totalMaterials = 0; while (totalMaterials < materials.size()) { unsigned char value; unsigned count; encodedData >> value; count = std::min(readCountValue(encodedData), static_cast(materials.size() - totalMaterials)); RBXASSERT(count > 0); memset(&materials[totalMaterials], value, count); totalMaterials += count; } // Set all cells Region3int16 region = SpatialRegion::inclusiveVoxelExtentsOfRegion(SpatialRegion::Id(chunkPos)); Vector3int16 startLocation = region.getMinPos(); unsigned int offset = 0; bool chunkWasEmpty = grid.getRegion(region.getMinPos(), region.getMaxPos()).isGuaranteedAllEmpty(); for (int y = 0; y < Voxel::kY_CHUNK_SIZE; ++y) { for (int z = 0; z < Voxel::kXZ_CHUNK_SIZE; ++z) { for (int x = 0; x < Voxel::kXZ_CHUNK_SIZE; ++x) { Cell cell = Voxel::Cell::readUnsignedCharFromFile(cells[offset]); // We only need to set non-empty cells if the chunk was empty before we started if (!(chunkWasEmpty && cell.isEmpty())) grid.setCell(startLocation + Vector3int16(x, y, z), cell, static_cast(materials[offset])); offset++; } } } } template void MegaClusterInstance::encodeChunkDataIntoStream( const Voxel::Grid::Region& chunk, Stream& s ) { if (chunk.isGuaranteedAllEmpty()) { s << Voxel::Cell::convertToUnsignedCharForFile(Voxel::Constants::kUniqueEmptyCellRepresentation); writeCountValue(s, Voxel::kXZ_CHUNK_SIZE * Voxel::kY_CHUNK_SIZE * Voxel::kXZ_CHUNK_SIZE); s << static_cast(Voxel::CELL_MATERIAL_Water); writeCountValue(s, Voxel::kXZ_CHUNK_SIZE * Voxel::kY_CHUNK_SIZE * Voxel::kXZ_CHUNK_SIZE); return; } unsigned char value = 0; int count = 0; for (Voxel::Grid::Region::iterator it = chunk.begin(); it != chunk.end(); ++it) { unsigned char temp = Voxel::Cell::convertToUnsignedCharForFile(it.getCellAtCurrentLocation()); if( value != temp && count != 0 ) { s << value; writeCountValue(s, count); count = 0; } value = temp; count++; } // encode the final value/count pair RBXASSERT( count > 0 ); s << value; writeCountValue(s, count); // Next try to encode new material byte value = 0; count = 0; for (Voxel::Grid::Region::iterator it = chunk.begin(); it != chunk.end(); ++it) { Voxel::CellMaterial temp = it.getMaterialAtCurrentLocation(); // 0xffff should fit entire chunk if necessary if( value != temp && count != 0 ) { s << value; writeCountValue(s, count); count = 0; } value = temp; count++; } // encode the final value/count pair RBXASSERT(count > 0); s << value; writeCountValue(s, count); } void MegaClusterInstance::incrementCellInChunkIndex( Vector3int16& index ) const { index.x++; if( index.x >= kXZ_CHUNK_SIZE ) { index.x = 0; index.z++; } if( index.z >= kXZ_CHUNK_SIZE ) { index.z = 0; index.y++; } } void MegaClusterInstance::setPackagedClusterGridV1(const std::string& clusterData) { if (clusterData.empty()) return; initializeGridMega(); // iterate over all chunks and decode them Vector3int16 numChunks = kLegacyChunkCount; Vector3int16 chunkPosOffset = kLegacyChunkOffset; StringReadBuffer stream(clusterData); for (int i = 0; i < numChunks.x; ++i) for (int j = 0; j < numChunks.y; ++j) for (int k = 0; k < numChunks.z; ++k) { decodeChunkDataFromStreamV1_Deprecated(Vector3int16(i, j, k) - chunkPosOffset, stream); if (stream.eof()) return; } } void MegaClusterInstance::setPackagedClusterGridV2(const std::string& clusterData) { if (clusterData.empty()) return; initializeGridMega(); // iterate over all chunks and decode them Vector3int16 numChunks = kLegacyChunkCount; Vector3int16 chunkPosOffset = kLegacyChunkOffset; StringReadBuffer stream(clusterData); for (int i = 0; i < numChunks.x; ++i) for (int j = 0; j < numChunks.y; ++j) for (int k = 0; k < numChunks.z; ++k) { decodeChunkDataFromStream(*voxelGrid, Vector3int16(i, j, k) - chunkPosOffset, stream); if (stream.eof()) return; } } void MegaClusterInstance::setPackagedClusterGridV3(const BinaryString& clusterData) { if (clusterData.value().empty()) return; initializeGridMega(); deserializeGridV3(*voxelGrid, clusterData); } BinaryString MegaClusterInstance::getPackagedClusteredGridV3() const { if (voxelGrid) return serializeGridV3(*voxelGrid); else return BinaryString(); } void MegaClusterInstance::setPackagedSmoothGrid(const BinaryString& clusterData) { if (clusterData.value().empty()) return; initializeGridSmooth(); smoothGrid->deserialize(clusterData.value()); } BinaryString MegaClusterInstance::getPackagedSmoothGrid() const { if (smoothGrid) { std::string result; smoothGrid->serialize(result); return BinaryString(result); } else return BinaryString(); } BinaryString MegaClusterInstance::serializeGridV3(const Voxel::Grid& grid) { if (grid.getNonEmptyCellCount() == 0) return BinaryString(); StringWriteBuffer encodedData; // iterate over chunks std::vector chunks = grid.getNonEmptyChunks(); for (size_t i = 0; i < chunks.size(); ++i) { SpatialRegion::Id id = chunks[i]; Region3int16 region = SpatialRegion::inclusiveVoxelExtentsOfRegion(id); Voxel::Grid::Region chunk = grid.getRegion(region.getMinPos(), region.getMaxPos()); writeInt16(encodedData, id.value().x); writeInt16(encodedData, id.value().y); writeInt16(encodedData, id.value().z); encodeChunkDataIntoStream(chunk, encodedData); } return BinaryString(encodedData.str()); } void MegaClusterInstance::deserializeGridV3(Voxel::Grid& grid, const BinaryString& data) { if (data.value().empty()) return; StringReadBuffer stream(data.value()); do { short chunkX = readInt16(stream); short chunkY = readInt16(stream); short chunkZ = readInt16(stream); decodeChunkDataFromStream(grid, Vector3int16(chunkX, chunkY, chunkZ), stream); } while (!stream.eof()); } void MegaClusterInstance::setCellInternalV1_Deprecated( const Vector3int16& pos, const unsigned char& inputCell) { Cell cell; CellMaterial material = getCellMaterial_Deprecated(inputCell); if (material == CELL_MATERIAL_Deprecated_Empty) { cell = Constants::kUniqueEmptyCellRepresentation; material = CELL_MATERIAL_Water; } else { Cell tmp = Cell::readUnsignedCharFromFile(inputCell); cell.solid.setBlock(tmp.solid.getBlock()); cell.solid.setOrientation(tmp.solid.getOrientation()); } voxelGrid->setCell(pos, cell, material); } shared_ptr MegaClusterInstance::getWaterCellScript(int x, int y, int z) { validateApi(); if (smoothGrid) { Voxel2::Cell cell = smoothGrid->getCell(x, y, z); shared_ptr result(new Reflection::Tuple(3)); result->values[0] = (cell.getMaterial() == Voxel2::Cell::Material_Water); result->values[1] = WATER_CELL_FORCE_None; result->values[2] = WATER_CELL_DIRECTION_NegX; return result; } else { Cell waterCell = voxelGrid->getWaterCell(Vector3int16(x,y,z)); shared_ptr result(new Reflection::Tuple(3)); if ( waterCell.isEmpty() ) { result->values[0] = false; result->values[1] = (WaterCellForce)0; result->values[2] = (WaterCellDirection)0; } else { result->values[0] = true; result->values[1] = waterCell.water.getForce(); result->values[2] = waterCell.water.getDirection(); } return result; } } void MegaClusterInstance::setWaterCellScript( int x, int y, int z, WaterCellForce force, WaterCellDirection direction ) { validateApi(); if (smoothGrid) { setCellsScript(Region3int16(Vector3int16(x, y, z), Vector3int16(x, y, z)), CELL_MATERIAL_Water, CELL_BLOCK_Solid, CELL_ORIENTATION_NegZ); } else { Vector3int16 pos = Vector3int16(x,y,z); Cell cell; cell.solid.setBlock(CELL_BLOCK_Empty); cell.water.setForceAndDirection(force, direction); voxelGrid->setCell(pos, cell, CELL_MATERIAL_Water); } } void MegaClusterInstance::setCellScript(int x, int y, int z, CellMaterial material, CellBlock block, CellOrientation orientation ) { validateApi(); if (smoothGrid) { setCellsScript(Region3int16(Vector3int16(x, y, z), Vector3int16(x, y, z)), material, block, orientation); } else { Vector3int16 pos = Vector3int16(x,y,z); if (material == CELL_MATERIAL_Water) { setWaterCellScript(x,y,z, (WaterCellForce)0, (WaterCellDirection)0); return; } Cell cell; if (material == CELL_MATERIAL_Deprecated_Empty || block == CELL_BLOCK_Empty) { cell = Constants::kUniqueEmptyCellRepresentation; material = CELL_MATERIAL_Water; } else { cell.solid.setBlock(block); cell.solid.setOrientation(orientation); } voxelGrid->setCell(pos, cell, material); } } bool MovePosition(Vector3int16& pos, FaceDirection dir) { switch(dir) { case PlusX: pos.x += 1; return true; case PlusZ: pos.z += 1; return true; case MinusX: if(pos.x == 0) return false; pos.x -= 1; return true; case MinusZ: if(pos.z == 0) return false; pos.z -= 1; return true; case PlusY: pos.y += 1; return true; case MinusY: if(pos.y == 0) return false; pos.y -= 1; return true; default: break; } return false; } Vector3 MegaClusterInstance::worldToCellPreferSolidScript(Vector3 worldPos ) { Vector3int16 result = worldToCellWithPreference( worldPos, true ); return Vector3( result.x, result.y, result.z ); } Vector3 MegaClusterInstance::worldToCellPreferEmptyScript(Vector3 worldPos ) { Vector3int16 result = worldToCellWithPreference( worldPos, false ); return Vector3( result.x, result.y, result.z ); } Vector3 MegaClusterInstance::worldToCellScript(Vector3 worldPos ) { Vector3int16 result = Voxel::worldToCell_floor(worldPos); return Vector3( result.x, result.y, result.z ); } bool MegaClusterInstance::isCellIdealForPreference( const Vector3int16& location, const Cell cell, bool preferSolid) const { Cell tmpCell = voxelGrid->getCell(location); return ((preferSolid && cell.solid.getBlock() != CELL_BLOCK_Empty) || (!preferSolid && tmpCell.isEmpty())); } bool MegaClusterInstance::isCellAcceptablePreferenceWaterAlternative( const Vector3int16& location, const Cell cell, bool preferSolid) const { Cell waterCell = voxelGrid->getWaterCell(location); bool hasWater = !waterCell.isEmpty(); return ((preferSolid && hasWater) || (!preferSolid && hasWater && cell.solid.getBlock() == CELL_BLOCK_Empty)); } struct WorldToCellCandidate { Vector3int16 pos; float distance; WorldToCellCandidate() { distance = 0; } WorldToCellCandidate(const Vector3& worldPos, int ox, int oy, int oz) { Vector3int16 center = Voxel::worldToCell_floor(worldPos); pos = center + Vector3int16(ox, oy, oz); distance = (worldPos - Voxel::cellToWorld_center(pos)).squaredLength(); } bool operator<(const WorldToCellCandidate& other) const { return distance < other.distance; } static bool isValid(const Voxel2::Cell& cell, bool preferSolid) { return (cell.getMaterial() != Voxel2::Cell::Material_Air) == preferSolid; } }; Vector3int16 MegaClusterInstance::worldToCellWithPreference(const Vector3& worldPos, bool preferSolid) { validateApi(); if (smoothGrid) { Vector3int16 center = Voxel::worldToCell_floor(worldPos); if (WorldToCellCandidate::isValid(smoothGrid->getCell(center.x, center.y, center.z), preferSolid)) return center; std::vector candidates; candidates.reserve(27); for (int x = -1; x <= 1; ++x) for (int y = -1; y <= 1; ++y) for (int z = -1; z <= 1; ++z) { candidates.push_back(WorldToCellCandidate(worldPos, x, y, z)); } std::sort(candidates.begin(), candidates.end()); for (auto& c: candidates) if (WorldToCellCandidate::isValid(smoothGrid->getCell(c.pos.x, c.pos.y, c.pos.z), preferSolid)) return c.pos; return center; } else { Vector3int16 firstApproximation = Voxel::worldToCell_floor(worldPos); Cell tmpCell = voxelGrid->getCell(firstApproximation); if (isCellIdealForPreference(firstApproximation, tmpCell, preferSolid)) { return firstApproximation; } bool foundAcceptableWaterCell = false; Vector3int16 acceptableWaterCell; const float searchEpsilon = 0.00048828125f; // 1 / 2048 for (int y = -1; y < 2; y += 2) { for (int z = -1; z < 2; z += 2) { for (int x = -1; x < 2; x += 2) { Vector3int16 test = Voxel::worldToCell_floor(worldPos + Vector3(searchEpsilon * x, searchEpsilon * y, searchEpsilon * z)); tmpCell = voxelGrid->getCell(test); if (isCellIdealForPreference(test, tmpCell, preferSolid)) { return test; } if (isCellAcceptablePreferenceWaterAlternative(test, tmpCell, preferSolid)) { foundAcceptableWaterCell = true; acceptableWaterCell = test; } } } } if (foundAcceptableWaterCell) { return acceptableWaterCell; } Vector3int16 bestCell = firstApproximation; bool foundTotalMatch = false; float bestDistance = 10; foundAcceptableWaterCell = false; float bestWaterDistance = 10; // Ok, now we need to search for empty around it for(unsigned i = 0; i < Invalid; i++) { Vector3int16 npos = firstApproximation; if(!MovePosition(npos, (FaceDirection)i)) continue; float distance = (cellToWorldExtents(npos).center() - worldPos).squaredLength(); tmpCell = voxelGrid->getCell(npos); if (isCellIdealForPreference(npos, tmpCell, preferSolid) && distance < bestDistance) { foundTotalMatch = true; bestCell = npos; bestDistance = distance; } if (isCellAcceptablePreferenceWaterAlternative(npos, tmpCell, preferSolid) && distance < bestWaterDistance) { foundAcceptableWaterCell = true; acceptableWaterCell = npos; bestWaterDistance = distance; } } if (!foundTotalMatch && foundAcceptableWaterCell) { return acceptableWaterCell; } else { return bestCell; } } } inline Vector3int16 floorToInt(const Vector3& vec) { return Vector3int16((G3D::int16)floor(vec.x), (G3D::int16)floor(vec.y), (G3D::int16)floor(vec.z)); } Vector3 MegaClusterInstance::cellCornerToWorld( const Vector3int16& v ) const { return Voxel::cellToWorld_smallestCorner(v); } Vector3 MegaClusterInstance::cellCenterToWorldScript( int x, int y, int z ) { return cellCornerToWorldScript( x, y, z ) + Vector3(2,2,2); } void MegaClusterInstance::clear() { validateApi(); if (smoothGrid) { std::vector regions = smoothGrid->getNonEmptyRegions(); for (size_t i = 0; i < regions.size(); ++i) { Vector3int32 size = regions[i].size(); Voxel2::Box box(size.x, size.y, size.z); smoothGrid->write(regions[i], box); } } else { std::vector chunks = voxelGrid->getNonEmptyChunks(); for (size_t i = 0; i < chunks.size(); ++i) { SpatialRegion::Id id = chunks[i]; Region3int16 region = SpatialRegion::inclusiveVoxelExtentsOfRegion(id); Vector3int16 min = region.getMinPos(); Vector3int16 max = region.getMaxPos(); for (int y = min.y; y <= max.y; ++y) for (int z = min.z; z <= max.z; ++z) for (int x = min.x; x <= max.x; ++x) voxelGrid->setCell(Vector3int16(x,y,z), Constants::kUniqueEmptyCellRepresentation, CELL_MATERIAL_Water); } } } int MegaClusterInstance::countCellsScript() { return getNonEmptyCellCount(); } const Region3int16 MegaClusterInstance::getMaxExtents() const { return Voxel::getTerrainExtentsInCells(); } void MegaClusterInstance::render3dSelect(Adorn* adorn, SelectState selectState) { // do nothing. we don't want to render the selection box } Extents MegaClusterInstance::cellToWorldExtents( const Vector3int16& cellpos ) { return Extents::fromCenterRadius(Voxel::cellToWorld_center(cellpos), kHALF_CELL); } Extents MegaClusterInstance::computeExtentsWorld() const { Extents result; if (smoothGrid) { std::vector regions = smoothGrid->getNonEmptyRegions(); for (size_t i = 0; i < regions.size(); ++i) { result.expandToContain(Voxel::cellSpaceToWorldSpace(regions[i].begin().toVector3())); result.expandToContain(Voxel::cellSpaceToWorldSpace(regions[i].end().toVector3())); } } else { std::vector chunks = voxelGrid->getNonEmptyChunks(); for (size_t i = 0; i < chunks.size(); ++i) { result.expandToContain(SpatialRegion::smallestCornerOfRegionInGlobalCoordStuds(chunks[i]).toVector3()); result.expandToContain(SpatialRegion::largestCornerOfRegionInGlobalCoordStuds(chunks[i]).toVector3()); } } return result; } shared_ptr MegaClusterInstance::copyRegion(Region3int16 region) { validateApi(); if (smoothGrid) return Creatable::create(smoothGrid.get(), region); else return Creatable::create(voxelGrid.get(), region); } void MegaClusterInstance::pasteRegion(shared_ptr region, Vector3int16 corner, bool pasteEmptyCells) { validateApi(); if (TerrainRegion* terrainRegion = fastDynamicCast(region.get())) { if (smoothGrid) terrainRegion->copyTo(*smoothGrid, corner, pasteEmptyCells); else terrainRegion->copyTo(*voxelGrid, corner, pasteEmptyCells); } else throw std::runtime_error("region has to be a TerrainRegion"); } void MegaClusterInstance::reloadMaterialTable() { if (!smoothGrid) return; materialTable.reset(new Voxel2::MaterialTable(ContentProvider::findAsset(kSmoothTerrainMaterials), Voxel2::Cell::Material_Max + 1)); Primitive* myPrim = getPartPrimitive(); DenseHashSet primitives(NULL); DenseHashSet contacts(NULL); for (int i = 0; i < myPrim->getNumContacts(); ++i) { primitives.insert(myPrim->getContactOther(i)); contacts.insert(myPrim->getContact(i)); } for (DenseHashSet::const_iterator it = contacts.begin(); it != contacts.end(); ++it) myPrim->getWorld()->destroyContact(*it); static_cast(myPrim->getGeometry())->updateAllChunks(); for (DenseHashSet::const_iterator it = primitives.begin(); it != primitives.end(); ++it) myPrim->getWorld()->getContactManager()->checkTerrainContact(*it); } Voxel::Grid* MegaClusterInstance::getVoxelGrid() { RBXASSERT(voxelGrid); return voxelGrid.get(); } Voxel2::Grid* MegaClusterInstance::getSmoothGrid() { RBXASSERT(smoothGrid); return smoothGrid.get(); } bool MegaClusterInstance::isSmooth() const { return !!smoothGrid; } bool MegaClusterInstance::isInitialized() const { return voxelGrid || smoothGrid; } void MegaClusterInstance::setSmoothReplicate(int value) { if (smoothReplicate != value) { smoothReplicate = value; raisePropertyChanged(desc_SmoothReplicate); } } unsigned int MegaClusterInstance::getNonEmptyCellCount() const { if (smoothGrid) return smoothGrid->getNonEmptyCellCountApprox(); if (voxelGrid) return voxelGrid->getNonEmptyCellCount(); return 0; } void MegaClusterInstance::convertToSmooth() { validateApi(); if (smoothGrid) throw std::runtime_error("Terrain is already smooth!"); if (Network::Players::getGameMode(this) != Network::EDIT || Network::Players::isCloudEdit(this)) { throw std::runtime_error("Terrain can only be converted in edit mode"); } // Unparent to get ourselves disconnected from CHS/etc Instance* parent = getParent(); setLockedParent(NULL); // Stash old grid somewhere and reinitialize as smooth grid scoped_ptr oldGrid; voxelGrid.swap(oldGrid); initializeGridSmooth(); // Transfer data from voxel grid into smooth grid Voxel2::Conversion::convertToSmooth(*oldGrid, *smoothGrid); // Reparent ourselves back setLockedParent(parent); // Set a waypoint so that further editing operations are undone up until this point ChangeHistoryService* chs = ServiceProvider::find(parent); if (chs) chs->resetBaseWaypoint(); raisePropertyChanged(prop_IsSmooth); } void MegaClusterInstance::initializeGridMega() { RBXASSERT(!getParent()); RBXASSERT(!voxelGrid && !smoothGrid); voxelGrid.reset(new Voxel::Grid()); getPartPrimitive()->setGeometryType(Geometry::GEOMETRY_MEGACLUSTER); smoothReplicate = 0; } void MegaClusterInstance::initializeGridSmooth() { RBXASSERT(!getParent()); RBXASSERT(!voxelGrid && !smoothGrid); smoothGrid.reset(new Voxel2::Grid()); getPartPrimitive()->setGeometryType(Geometry::GEOMETRY_SMOOTHCLUSTER); smoothReplicate = 1; } static Voxel2::Region getRegionAtResolution(const Region3& region, float resolution) { if (resolution != 4) throw std::runtime_error("Resolution has to be 4"); Vector3 min = region.minPos() / resolution; Vector3 max = region.maxPos() / resolution; Vector3int32 imin(min.x, min.y, min.z); Vector3int32 imax(max.x, max.y, max.z); if (imin.toVector3() != min || imax.toVector3() != max) throw std::runtime_error("Region has to be aligned to the grid (use Region3:ExpandToGrid)"); if (imin.x >= imax.x || imin.y >= imax.y || imin.z >= imax.z) throw std::runtime_error("Region cannot be empty"); Voxel2::Region result(imin, imax); // One cell is represented as two Lua values, resulting in ~32b/cell overhead // 4M voxels would take 128Mb if (result.getChunkCount(0) > FInt::SmoothTerrainMaxLuaRegion) throw std::runtime_error("Region is too large"); return result; } static Voxel2::Region getRegionFromExtents(const Vector3& min, const Vector3& max) { Voxel2::Region result = Voxel2::Region::fromExtents(min, max); if (result.empty()) throw std::runtime_error("Extents cannot be empty"); // 64M voxels would take 128Mb if (result.getChunkCount(0) > FInt::SmoothTerrainMaxCppRegion) throw std::runtime_error("Extents are too large"); return result; } template struct FixedSizeCache { K keys[N]; V values[N]; size_t next; FixedSizeCache(K invalid): next(0) { for (size_t i = 0; i < N; ++i) keys[i] = invalid; } std::pair insert(K key) { for (size_t i = 0; i < N; ++i) if (keys[i] == key) return std::make_pair(false, i); size_t index = next; keys[index] = key; next = (next + 1) % N; return std::make_pair(true, index); } size_t size() const { return N; } }; int MegaClusterInstance::readVoxels(lua_State* L) { validateApiSmooth(); Lua::LuaArguments args(L, 1); Region3 region; if (!args.getRegion3(1, region)) throw std::runtime_error("Argument 1 missing or nil"); double resolution; if (!args.getDouble(2, resolution)) throw std::runtime_error("Argument 2 missing or nil"); Voxel2::Region rr = getRegionAtResolution(region, resolution); const Voxel2::Box box = smoothGrid->read(rr); const Reflection::EnumDesc& matDesc = Reflection::EnumDesc::singleton(); FixedSizeCache materialCache(Voxel2::Cell::Material_Max + 1); int materialCacheStack = lua_gettop(L); for (size_t i = 0; i < materialCache.size(); ++i) lua_pushnil(L); lua_createtable(L, box.getSizeX(), 0); // mat lua_createtable(L, box.getSizeX(), 0); // occ for (int x = 0; x < box.getSizeX(); ++x) { lua_createtable(L, box.getSizeY(), 0); // matX lua_createtable(L, box.getSizeY(), 0); // occX for (int y = 0; y < box.getSizeY(); ++y) { lua_createtable(L, box.getSizeZ(), 0); // matY lua_createtable(L, box.getSizeZ(), 0); // occY for (int z = 0; z < box.getSizeZ(); ++z) { const Voxel2::Cell& cell = box.get(x, y, z); std::pair materialCacheIndex = materialCache.insert(cell.getMaterial()); // make sure material cache is up to date if (materialCacheIndex.first) { PartMaterial material = Voxel2::Conversion::getMaterialFromVoxelMaterial(cell.getMaterial()); Lua::EnumItem::push(L, matDesc.convertToItem(material)); lua_replace(L, materialCacheStack + materialCacheIndex.second); } // push material lua_pushvalue(L, materialCacheStack + materialCacheIndex.second); // push occupancy if (cell.getMaterial() == Voxel2::Cell::Material_Air) { lua_pushnumber(L, 0); } else { float occScale = 1.f / (Voxel2::Cell::Occupancy_Max + 1); lua_pushnumber(L, (cell.getOccupancy() + 1) * occScale); } lua_rawseti(L, -3, z + 1); lua_rawseti(L, -3, z + 1); } lua_rawseti(L, -3, y + 1); lua_rawseti(L, -3, y + 1); } lua_rawseti(L, -3, x + 1); lua_rawseti(L, -3, x + 1); } lua_pushstring(L, "Size"); Lua::Vector3Bridge::pushVector3(L, box.getSize().toVector3()); lua_rawset(L, -4); lua_pushstring(L, "Size"); Lua::Vector3Bridge::pushVector3(L, box.getSize().toVector3()); lua_rawset(L, -3); return 2; } static bool isArray(lua_State* L, int index, int size) { return lua_istable(L, index) && lua_objlen(L, index) == size; } int MegaClusterInstance::writeVoxels(lua_State* L) { validateApiSmooth(); Lua::LuaArguments args(L, 1); Region3 region; if (!args.getRegion3(1, region)) throw std::runtime_error("Argument 1 missing or nil"); double resolution; if (!args.getDouble(2, resolution)) throw std::runtime_error("Argument 2 missing or nil"); if (4 > lua_gettop(L)) throw std::runtime_error("Argument 3 missing or nil"); if (5 > lua_gettop(L)) throw std::runtime_error("Argument 4 missing or nil"); Voxel2::Region rr = getRegionAtResolution(region, resolution); Voxel2::Box box(rr.size().x, rr.size().y, rr.size().z); const Reflection::EnumDesc& matDesc = Reflection::EnumDesc::singleton(); FixedSizeCache materialCache(NULL); if (!isArray(L, 4, box.getSizeX())) throw RBX::runtime_error("Bad argument materials to 'WriteVoxels' (%dx%dx%d array expected)", box.getSizeX(), box.getSizeY(), box.getSizeZ()); if (!isArray(L, 5, box.getSizeX())) throw RBX::runtime_error("Bad argument occupancy to 'WriteVoxels' (%dx%dx%d array expected)", box.getSizeX(), box.getSizeY(), box.getSizeZ()); for (int x = 0; x < box.getSizeX(); ++x) { lua_rawgeti(L, 4, x + 1); // matX lua_rawgeti(L, 5, x + 1); // occX if (!isArray(L, -2, box.getSizeY())) throw RBX::runtime_error("Bad argument materials[%d] to 'WriteVoxels' (%dx%d array expected)", x + 1, box.getSizeY(), box.getSizeZ()); if (!isArray(L, -1, box.getSizeY())) throw RBX::runtime_error("Bad argument occupancy[%d] to 'WriteVoxels' (%dx%d array expected)", x + 1, box.getSizeY(), box.getSizeZ()); for (int y = 0; y < box.getSizeY(); ++y) { lua_rawgeti(L, -2, y + 1); // matY lua_rawgeti(L, -2, y + 1); // occY if (!isArray(L, -2, box.getSizeZ())) throw RBX::runtime_error("Bad argument materials[%d][%d] to 'WriteVoxels' (%d-element array expected)", x + 1, y + 1, box.getSizeZ()); if (!isArray(L, -1, box.getSizeZ())) throw RBX::runtime_error("Bad argument occupancy[%d][%d] to 'WriteVoxels' (%d-element array expected)", x + 1, y + 1, box.getSizeZ()); for (int z = 0; z < box.getSizeZ(); ++z) { lua_rawgeti(L, -2, z + 1); // matZ lua_rawgeti(L, -2, z + 1); // occZ void* mat = lua_touserdata(L, -2); if (!mat) luaL_error(L, "Bad argument materials[%d][%d][%d] to 'WriteVoxels' (Enum.Material expected, got %s)", x + 1, y + 1, z + 1, luaL_typename(L, -2)); std::pair materialCacheIndex = materialCache.insert(mat); if (materialCacheIndex.first) { const Reflection::EnumDescriptor::Item* matItemDesc = NULL; if (!Lua::EnumItem::getItem(L, -2, matItemDesc)) luaL_error(L, "Bad argument materials[%d][%d][%d] to 'WriteVoxels' (Enum.Material expected, got %s)", x + 1, y + 1, z + 1, luaL_typename(L, -2)); if (&matItemDesc->owner != &matDesc) luaL_error(L, "Bad argument materials[%d][%d][%d] to 'WriteVoxels' (Enum.Material expected, got %s)", x + 1, y + 1, z + 1, matItemDesc->owner.name.c_str()); unsigned char material = Voxel2::Conversion::getVoxelMaterialFromMaterial(static_cast(matItemDesc->value)); materialCache.values[materialCacheIndex.second] = material; } unsigned char material = materialCache.values[materialCacheIndex.second]; if (!lua_isnumber(L, -1)) luaL_error(L, "Bad argument occupancy[%d][%d][%d] to 'WriteVoxels' (number expected, got %s)", x + 1, y + 1, z + 1, luaL_typename(L, -1)); float occ = lua_tonumber(L, -1); if (material == Voxel2::Cell::Material_Air || Math::isNanInf(occ) || occ <= 0) { box.set(x, y, z, Voxel2::Cell()); } else { // We round occupancy to nearest integer value, keeping in mind that 1/256.f means 0 in Cell float occInt = occ * (Voxel2::Cell::Occupancy_Max + 1) - 0.5f; box.set(x, y, z, Voxel2::Cell(material, std::min(std::max(static_cast(occInt), 0), static_cast(Voxel2::Cell::Occupancy_Max)))); } lua_pop(L, 2); } lua_pop(L, 2); } lua_pop(L, 2); } smoothGrid->write(rr, box); return 0; } void MegaClusterInstance::fillRegion(Region3 region, float resolution, PartMaterial material) { validateApiSmooth(); Voxel2::Region rr = getRegionAtResolution(region, resolution); Vector3int32 rsize = rr.size(); Voxel2::Box box(rsize.x, rsize.y, rsize.z); unsigned char voxelMaterial = Voxel2::Conversion::getVoxelMaterialFromMaterial(material); if (voxelMaterial != Voxel2::Cell::Material_Air) { Voxel2::Cell cell(voxelMaterial, Voxel2::Cell::Occupancy_Max); for (int y = 0; y < rsize.y; ++y) for (int z = 0; z < rsize.z; ++z) { Voxel2::Cell* row = box.writeRow(0, y, z); for (int x = 0; x < rsize.x; ++x) row[x] = cell; } } smoothGrid->write(rr, box); } static void fillCell(Voxel2::Cell& cell, unsigned char material, int occ) { if (material == Voxel2::Cell::Material_Air) { if (cell.getOccupancy() <= occ) cell = Voxel2::Cell(); else { int aocc = Voxel2::Cell::Occupancy_Max - occ; if (cell.getOccupancy() > aocc) cell = Voxel2::Cell(cell.getMaterial(), aocc); } } else { if (cell.getOccupancy() <= occ && occ != 0) cell = Voxel2::Cell(material, occ); } } void MegaClusterInstance::fillBlock(CoordinateFrame cframe, Vector3 size, PartMaterial material) { validateApiSmooth(); AABox aabb = cframe.AABBtoWorldSpace(AABox(-size / 2, size / 2)); Voxel2::Region rr = getRegionFromExtents(aabb.low(), aabb.high()); Vector3int32 rsize = rr.size(); Voxel2::Box box = smoothGrid->read(rr); unsigned char voxelMaterial = Voxel2::Conversion::getVoxelMaterialFromMaterial(material); if (voxelMaterial == Voxel2::Cell::Material_Air && box.isEmpty()) return; // Since we only care about the size if it's less than one cell, we clamp this to make the calculations below faster. Vector3 sizeCellsClamped = (size / Voxel::kCELL_SIZE).min(Vector3(1.0f, 1.0f, 1.0f)); Vector3 sizeCellsHalfOffset = size * (0.5f / Voxel::kCELL_SIZE) + Vector3(0.5f, 0.5f, 0.5f); Vector3 localCorner = cframe.pointToObjectSpace(Vector3(rr.begin().x + 0.5f, rr.begin().y + 0.5f, rr.begin().z + 0.5f) * Voxel::kCELL_SIZE) / Voxel::kCELL_SIZE; Vector3 localAxisX = Vector3(cframe.rotation[0][0], cframe.rotation[0][1], cframe.rotation[0][2]); Vector3 localAxisY = Vector3(cframe.rotation[1][0], cframe.rotation[1][1], cframe.rotation[1][2]); Vector3 localAxisZ = Vector3(cframe.rotation[2][0], cframe.rotation[2][1], cframe.rotation[2][2]); Vector3 localY = localCorner; for (int y = 0; y < rsize.y; ++y) { Vector3 localZ = localY; for (int z = 0; z < rsize.z; ++z) { Vector3 localX = localZ; Voxel2::Cell* row = box.writeRow(0, y, z); for (int x = 0; x < rsize.x; ++x) { float distX = sizeCellsHalfOffset.x - fabsf(localX.x); float distY = sizeCellsHalfOffset.y - fabsf(localX.y); float distZ = sizeCellsHalfOffset.z - fabsf(localX.z); // if distance <-0.5, voxel is completely inside the part // if distance >+0.5, voxel is completely outside the part // note that we include 0.5 offset in the distXYZ calculation float factorX = std::max(0.f, std::min(distX, sizeCellsClamped.x)); float factorY = std::max(0.f, std::min(distY, sizeCellsClamped.y)); float factorZ = std::max(0.f, std::min(distZ, sizeCellsClamped.z)); // occupancy is a linear metric, not volumetric // it is more correct to use powf(fx*fy*fz, 1/3.f), but it is slower float factor = std::min(factorX, std::min(factorY, factorZ)); int occ = static_cast(factor * Voxel2::Cell::Occupancy_Max + 0.5f); fillCell(row[x], voxelMaterial, occ); localX += localAxisX; } localZ += localAxisZ; } localY += localAxisY; } smoothGrid->write(rr, box); } void MegaClusterInstance::fillBall(Vector3 center, float radius, PartMaterial material) { fillBallInternal(center, radius, material, /* skipWater= */ false); } void MegaClusterInstance::fillBallInternal(Vector3 center, float radius, PartMaterial material, bool skipWater) { validateApiSmooth(); Voxel2::Region rr = getRegionFromExtents(center - Vector3(radius), center + Vector3(radius)); Vector3int32 rsize = rr.size(); Voxel2::Box box = smoothGrid->read(rr); unsigned char voxelMaterial = Voxel2::Conversion::getVoxelMaterialFromMaterial(material); if (voxelMaterial == Voxel2::Cell::Material_Air && box.isEmpty()) return; unsigned char skipMaterial = skipWater ? Voxel2::Cell::Material_Water : Voxel2::Cell::Material_Max + 1; Vector3 centerv = center / Voxel::kCELL_SIZE; float vradius = radius / Voxel::kCELL_SIZE; for (int y = 0; y < rsize.y; ++y) for (int z = 0; z < rsize.z; ++z) { Vector3 diff = Vector3(rr.begin().x, y + rr.begin().y, z + rr.begin().z) + Vector3(0.5) - centerv; Voxel2::Cell* row = box.writeRow(0, y, z); for (int x = 0; x < rsize.x; ++x, diff.x += 1) { float d = diff.length(); int occ = static_cast(G3D::clamp(vradius - d + 0.5f, 0.f, 1.f) * Voxel2::Cell::Occupancy_Max + 0.5f); if (row[x].getMaterial() != skipMaterial) fillCell(row[x], voxelMaterial, occ); } } smoothGrid->write(rr, box); } void MegaClusterInstance::validateApi() { if (!voxelGrid && !smoothGrid) throw std::runtime_error("Terrain API is not available"); } void MegaClusterInstance::validateApiSmooth() { if (!smoothGrid) throw std::runtime_error("Smooth terrain API is not available"); } void MegaClusterInstance::setWaterColor(const Color3& value) { if (waterColor != value) { waterColor = value; raisePropertyChanged(desc_WaterColor); } } void MegaClusterInstance::setWaterTransparency(float value) { value = G3D::clamp(value, 0.f, 1.f); if (waterTransparency != value) { waterTransparency = value; raisePropertyChanged(desc_WaterTransparency); } } void MegaClusterInstance::setWaterWaveSize(float value) { value = G3D::clamp(value, 0.f, 1.f); if (waterWaveSize != value) { waterWaveSize = value; raisePropertyChanged(desc_WaterWaveSize); } } void MegaClusterInstance::setWaterWaveSpeed(float value) { value = G3D::clamp(value, 0.f, 100.f); if (waterWaveSpeed != value) { waterWaveSpeed = value; raisePropertyChanged(desc_WaterWaveSpeed); } } } //namespace