Documentation and tidying.

This commit is contained in:
David Williams 2011-02-13 00:07:12 +00:00
parent 29e2e14c3a
commit ad0e923413
5 changed files with 124 additions and 114 deletions

View File

@ -382,7 +382,7 @@ void createPerlinVolumeSlow(Volume<MaterialDensityPair44>& volData)
}
}
void createPerlinVolumeFast(Volume<MaterialDensityPair44>& volData)
/*void createPerlinVolumeFast(Volume<MaterialDensityPair44>& volData)
{
Perlin perlin(2,8,1,234);
@ -428,7 +428,7 @@ void createPerlinVolumeFast(Volume<MaterialDensityPair44>& volData)
}
}
}
}
}*/
void createPerlinTerrain(Volume<MaterialDensityPair44>& volData)
{
@ -510,13 +510,14 @@ int main(int argc, char *argv[])
openGLWidget.show();
//Create an empty volume and then place a sphere in it
Volume<MaterialDensityPair44> volData(2048, 2048, 256);
Volume<MaterialDensityPair44> volData(256, 256, 256);
//createSphereInVolume(volData, 30);
createPerlinTerrain(volData);
//createPerlinVolumeSlow(volData);
std::cout << "Memory usage: " << volData.sizeInBytes() << std::endl;
std::cout << "Memory usage: " << volData.calculateSizeInBytes() << std::endl;
volData.setBlockCacheSize(8);
std::cout << "Memory usage: " << volData.sizeInBytes() << std::endl;
std::cout << "Memory usage: " << volData.calculateSizeInBytes() << std::endl;
std::cout << "Compression ratio: " << volData.calculateCompressionRatio() << std::endl;
/*srand(12345);
for(int ct = 0; ct < 1000; ct++)

View File

@ -58,7 +58,7 @@ namespace PolyVox
void fill(VoxelType tValue);
void initialise(uint16_t uSideLength);
uint32_t sizeInBytes(void);
uint32_t calculateSizeInBytes(void);
public:
void compress(void);

View File

@ -131,7 +131,7 @@ namespace PolyVox
}
template <typename VoxelType>
uint32_t Block<VoxelType>::sizeInBytes(void)
uint32_t Block<VoxelType>::calculateSizeInBytes(void)
{
uint32_t uSizeInBytes = sizeof(Block<VoxelType>);
uSizeInBytes += m_vecCompressedData.capacity() * sizeof(RunlengthEntry<uint16_t>);

View File

@ -36,86 +36,81 @@ freely, subject to the following restrictions:
namespace PolyVox
{
///The Volume class provides a memory efficient method of storing voxel data while also allowing fast access and modification.
////////////////////////////////////////////////////////////////////////////////
/// A Volume is essentially a '3D image' in which each element (voxel) is identified
/// by a three dimensional (x,y,z) coordinate, rather than the two dimensional (x,y)
/// coordinate which is used to identify an element (pixel) in a normal image. Within
/// PolyVox, the Volume class is used to store and manipulate our data before we extract
/// our SurfaceMeshs from it.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// A Volume is essentially a '3D image' in which each element (voxel) is identified by a three dimensional (x,y,z) coordinate,
/// rather than the two dimensional (x,y) coordinate which is used to identify an element (pixel) in a normal image. Within PolyVox,
/// the Volume class is used to store and manipulate our data before we extract our SurfaceMeshs from it.
///
/// <b>Data Representaion - feel free to skip</b>
/// If stored carelessly, volume data can take up a huge amount of memory. For example, a
/// volume of dimensions 1024x1024x1024 with 1 byte per voxel will require 1GB of memory
/// if stored in an uncompressed form. Natuarally our Volume class is much more efficient
/// than this and it is worth understanding (at least at a high level) the approach
/// which is used.
/// <b>Data Representaion</b>
/// If stored carelessly, volume data can take up a huge amount of memory. For example, a volume of dimensions 1024x1024x1024 with
/// 1 byte per voxel will require 1GB of memory if stored in an uncompressed form. Natuarally our Volume class is much more efficient
/// than this and it is worth understanding (at least at a high level) the approach which is used.
///
/// Essentially, the Volume class stores its data as a collection of blocks. Each of these block is much smaller than the whole volume,
/// for example a typical size might be 32x32x32 voxels (though is is configurable by the user). In this case, a 256x512x1024 volume
/// would contain 8x16x32 = 4096 blocks. The data for each block is stored in a compressed form, which uses only a small amout of
/// memory but it is hard to modify the data. Therefore, before any given voxel can be modified, its corresponding block must be uncompressed.
///
/// The compression and decompression of block is a relatively slow process and so we aim to do this as rarely as possible. In order
/// to achive this, the volume class store a cache of recently used blocks and their associated uncompressed data. Each time a voxel
/// is touched a timestamp is updated on the corresponding block, when the cache becomes full the block with the oldest timestamp is
/// recompressed and moved out of the cache.
///
/// <b>Achieving high compression rates</b>
/// Note: This section is theorectical and not well tested. Please let us know if you find the tips below do or do not work.
///
/// The compression rates which can be achieved can vary significantly depending the nature of the data you are storing, but you can
/// encourage high compression rates by making your data as homogenous as possible. If you are simply storing a material with each
/// voxel then this will probably happen naturally. Games such as Minecraft which use this approach will typically involve large ares
/// of th same material which will compress down well.
///
/// However, if you are storing density values then you may want to take some care. The advantage of storing smoothly changing values
/// is that you can get smooth surfaces extracted, but storing smoothly changing values inside or outside objects (rather than just
/// on the boundary) does not benefit the surface and is very hard to compress effectively. You should apply some thresholding to your
/// density values to reduce this problem (this threasholding should only be applied to voxels who don't contribute to the surface).
///
/// Essentially, the Volume class stores its data as a collection of blocks. Each
/// of these block is much smaller than the whole volume, for example a typical size
/// might be 32x32x32 voxels (though is is configurable by the user). In this case,
/// a 256x512x1024 volume would contain 8x16x32 = 4096 blocks. However, it is unlikely that
/// all these blocks actually have to be stored because usually there are duplicates
/// in which case common data can be shared.
///
/// Identifying duplicate blocks is in general a difficult task which involves looking at pairs
/// of blocks and comparing all the voxels. This is a time consuming task which is not amiable
/// to being performed when the volume is being modified in real time. However, there are two
/// specific scenarios which are easily spotted and which PolyVox uses to identify block
/// sharing opportunities.
///
/// -# Homogeneous blocks (those which contain just a single voxel value) are easy to
/// spot and fairly common becuase volumes often contain large homogeous regions. Any time
/// you change the value of a voxel you have potentially made the block which contains
/// it homogeneous. PolyVox does not check the homogeneity immediately as this would slow
/// down the process of modifying voxels, but you can use the tidyUpMemory() function
/// to check for and remove duplicate homogeneous regions whenever you have spare
/// processing time.
///
/// -# Copying a volume naturally means that all the voxels in the second volume are
/// the same as the first. Therefore volume copying is a relatively fast operation in
/// which all the blocks in the second volume simply reference the first volume. Future
/// modifications to either volume will, of course, cause the blocks to become unshared.
///
/// Other advantages of breaking the volume down into blocks include enhancing data locality
/// (i.e. voxels which are spatially near to each other are also likely to be near in
/// memory) and the ability to load larger volumes as no large contiguous areas of
/// memory are needed. However, these advantages are more transparent to user code
/// so we will not dwell on them here.
///
/// <b>Usage</b>
/// Volumes are constructed by passing the desired width, height and depth to the
/// constructor. Note that for speed reasons only values which are a power of two
/// are permitted for these sidelengths.
///
/// \code
/// Volume<uint8_t> volData(g_uVolumeSideLength, g_uVolumeSideLength, g_uVolumeSideLength);
/// \endcode
///
/// Access to specific voxels is provided by the getVoxelAt() and setVoxelAt() fuctions.
/// Each of these has two forms so that voxels can be identified by integer triples
/// or by Vector3DUint16%s.
///
/// \code
/// volData.setVoxelAt(12, 186, 281, 3);
/// uint8_t voxelValue = volData.getVoxelAt(12, 186, 281);
/// //voxelValue is now 3
/// \endcode
///
/// The tidyUpMemory() function should normally be called after you first populate
/// the volume with data, and then at periodic intervals as the volume is modified.
/// However, you don't actually <i>have</i> to call it at all. See the function's
/// documentation for further details.
///
/// One further important point of note is that this class is templatised on the voxel
/// type. This allows you to store volumes of data types you might not normally expect,
/// for example the OpenGL example 'abuses' this class to store a 3D grid of pointers.
/// However, it is not guaranteed that all functionality works correctly with non-integer
/// voxel types.
////////////////////////////////////////////////////////////////////////////////
/// For example, suppose you are using layers of 3D Perlin noise to create a 3D terrain (not a heightmap). If you store the raw Perlin
/// noise value at each voxel then a slice through the volume might look like the following:
///
/// <insert image here>
///
/// However, by setting high values to be fixed to one and low values to be fixed to zero you can make a slice through your volume look more like this:
///
/// <insert image here>
///
/// The boundary is in the same place and is still smooth, but the large homogenous regions mean the data should compress much more effectively.
/// Although it may look like you have lost some precision in this process this is only because the images above are constrained to 256
/// greyscale values, where as true Perlin noise will give you floating point values.
///
/// <b>Cache-aware traversal</b>
/// You might be suprised at just how many cache misses can occur when you traverse the volume in a naive manner. Consider a 1024x1024x1024 volume
/// with blocks of size 32x32x32. And imagine you iterate over this volume with a simple three-level for loop which iterates over x, the y, then z.
/// If you start at position (0,0,0) then ny the time you reach position (1023,0,0) you have touched 1024 voxels along one edge of the volume and
/// have pulled 32 blocks into the cache. By the time you reach (1023,1023,0) you have hit 1024x1024 voxels and pulled 32x32 blocks into the cache.
/// You are now ready to touch voxel (0,0,1) which is right nect to where you started, but unless your cache is at least 32x32 blocks large then this
/// initial block has already been cleared from the cache.
///
/// Ensuring you have a large enough cache size can obviously help the above situation, but you might also consider iterating over the voxels in a
/// different order. For example, if you replace your three-level loop with a six-level loop then you can first process all the voxels between (0,0,0)
/// and (31,31,31), then process all the voxels between (32,0,0) and (63,0,0), and so forth. Using this approach you will have no cache misses even
/// is your cache sise is only one. Of course the logic is more complex, but writing code in such a cache-aware manner may be beneficial in some situations.
///
/// <b>Threading</b>
/// The volume class does not provide any thread safety constructs and can therefore not be assumed to be thread safe. To be safe you should only allow
/// one thread to access the volume at a time. Even if you have several threads just reading data from the volume they can cause blocks to be pushed
/// out of the cache, potentially invalidating any pointers other threads might be using.
///
/// That said, we believe that if care is taken then multiple threads can be used, and are currently experimenting with this.
///
/// <b>Use of templates</b>
/// Although this class is templatised on the voxel type it is not expected that you can use any primative type to represent your voxels. It is only
/// intended for PolyVox's voxel types such as Material, Density, and MarterialDensityPair. If you need to store 3D grids of ints, floats, or pointers
/// you should look ar the Array class instead.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename VoxelType>
class Volume
{
//Make VolumeSampler a friend
// Make VolumeSampler a friend
friend class VolumeSampler<VoxelType>;
struct UncompressedBlock
@ -126,49 +121,49 @@ namespace PolyVox
};
public:
///Constructor
/// Constructor
Volume(uint16_t uWidth, uint16_t uHeight, uint16_t uDepth, uint16_t uBlockSideLength = 32);
///Destructor
/// Destructor
~Volume();
///Gets the value used for voxels which are outside the volume
/// Gets the value used for voxels which are outside the volume
VoxelType getBorderValue(void) const;
///Gets a Region representing the extents of the Volume.
/// Gets a Region representing the extents of the Volume.
Region getEnclosingRegion(void) const;
///Gets the width of the volume in voxels.
/// Gets the width of the volume in voxels.
uint16_t getWidth(void) const;
///Gets the height of the volume in voxels.
/// Gets the height of the volume in voxels.
uint16_t getHeight(void) const;
///Gets the depth of the volume in voxels.
/// Gets the depth of the volume in voxels.
uint16_t getDepth(void) const;
///Gets the length of the longest side in voxels
/// Gets the length of the longest side in voxels
uint16_t getLongestSideLength(void) const;
///Gets the length of the shortest side in voxels
/// Gets the length of the shortest side in voxels
uint16_t getShortestSideLength(void) const;
///Gets the length of the diagonal in voxels
/// Gets the length of the diagonal in voxels
float getDiagonalLength(void) const;
///Gets a voxel by <tt>x,y,z</tt> position
/// Gets a voxel by <tt>x,y,z</tt> position
VoxelType getVoxelAt(uint16_t uXPos, uint16_t uYPos, uint16_t uZPos) const;
///Gets a voxel by 3D vector position
/// Gets a voxel by 3D vector position
VoxelType getVoxelAt(const Vector3DUint16& v3dPos) const;
///Sets the value used for voxels which are outside the volume
/// Sets the number of blocks for which uncompressed data is stored.
void setBlockCacheSize(uint16_t uBlockCacheSize);
/// Sets the value used for voxels which are outside the volume
void setBorderValue(const VoxelType& tBorder);
///Sets the voxel at an <tt>x,y,z</tt> position
/// Sets the voxel at an <tt>x,y,z</tt> position
bool setVoxelAt(uint16_t uXPos, uint16_t uYPos, uint16_t uZPos, VoxelType tValue);
///Sets the voxel at a 3D vector position
/// Sets the voxel at a 3D vector position
bool setVoxelAt(const Vector3DUint16& v3dPos, VoxelType tValue);
///Resizes the volume to the specified dimensions
void resize(uint16_t uWidth, uint16_t uHeight, uint16_t uDepth, uint16_t uBlockSideLength = 32);
void setBlockCacheSize(uint16_t uBlockCacheSize);
void clearBlockCache(void);
float calculateCompressionRatio(void);
uint32_t calculateSizeInBytes(void);
uint32_t sizeInBytes(void);
public:
private:
Block<VoxelType>* getUncompressedBlock(uint16_t uBlockX, uint16_t uBlockY, uint16_t uBlockZ) const;
/// Resizes the volume to the specified dimensions
void resize(uint16_t uWidth, uint16_t uHeight, uint16_t uDepth, uint16_t uBlockSideLength = 32);
//The block data
mutable std::vector< Block<VoxelType> > m_pBlocks;

View File

@ -198,6 +198,20 @@ namespace PolyVox
return getVoxelAt(v3dPos.getX(), v3dPos.getY(), v3dPos.getZ());
}
////////////////////////////////////////////////////////////////////////////////
/// Increasing the size of the block cache will increase memory but may improve performance.
/// You may want to set this to a large value (e.g. 1024) when you are first loading your
/// volume data and then set it to a smaller value (e.g.64) for general processing.
/// \param uBlockCacheSize The number of blocks for which uncompressed data can be cached.
////////////////////////////////////////////////////////////////////////////////
template <typename VoxelType>
void Volume<VoxelType>::setBlockCacheSize(uint16_t uBlockCacheSize)
{
clearBlockCache();
m_uMaxUncompressedBlockCacheSize = uBlockCacheSize;
}
////////////////////////////////////////////////////////////////////////////////
/// \param tBorder The value to use for voxels outside the volume.
////////////////////////////////////////////////////////////////////////////////
@ -352,14 +366,6 @@ namespace PolyVox
m_fDiagonalLength = sqrtf(static_cast<float>(m_uWidth * m_uWidth + m_uHeight * m_uHeight + m_uDepth * m_uDepth));
}
template <typename VoxelType>
void Volume<VoxelType>::setBlockCacheSize(uint16_t uBlockCacheSize)
{
clearBlockCache();
m_uMaxUncompressedBlockCacheSize = uBlockCacheSize;
}
template <typename VoxelType>
Block<VoxelType>* Volume<VoxelType>::getUncompressedBlock(uint16_t uBlockX, uint16_t uBlockY, uint16_t uBlockZ) const
{
@ -424,14 +430,22 @@ namespace PolyVox
}
template <typename VoxelType>
uint32_t Volume<VoxelType>::sizeInBytes(void)
float Volume<VoxelType>::calculateCompressionRatio(void)
{
float fRawSize = m_uWidth * m_uHeight * m_uDepth * sizeof(VoxelType);
float fCompressedSize = calculateSizeInBytes();
return fCompressedSize/fRawSize;
}
template <typename VoxelType>
uint32_t Volume<VoxelType>::calculateSizeInBytes(void)
{
uint32_t uSizeInBytes = sizeof(Volume);
//Memory used by the blocks
for(uint32_t i = 0; i < m_uNoOfBlocksInVolume; ++i)
{
uSizeInBytes += m_pBlocks[i].sizeInBytes();
uSizeInBytes += m_pBlocks[i].calculateSizeInBytes();
}
//Memory used by the block cache.