Volumes Of Fun
http://www.volumesoffun.com/phpBB3/

How to use LowPassFilter
http://www.volumesoffun.com/phpBB3/viewtopic.php?f=14&t=530
Page 1 of 2

Author:  BlazeCell [ Tue Aug 20, 2013 7:29 pm ]
Post subject:  How to use LowPassFilter

Hello, I recently began looking into PolyVox as means to allow 3D terrain editing in my game engine. I've gone through the example projects that come with the distribution and generally understand what is going on.

I noticed that the marching cubes mesh generation produces a stepped, blocky looking mesh. Which, from my understanding of marching cubes, is to be expected. Looking through the source code for the OpenGL example I ran across a commented out section of code:
Code:
//I've removed this smoothing because it doesn't really make sense to apply a low pass filter to a volume with material values.
//I could implement the mathematical operators for MaterialDensityPair in such a way that they ignores the materials but this
//seems to be setting a bad example. Users can add this operators in their own classes if they want smoothing.
//RawVolume<MaterialDensityPair44> tempVolume(PolyVox::Region(0,0,0,128, 128, 128));
//LowPassFilter< LargeVolume<MaterialDensityPair44>, RawVolume<MaterialDensityPair44> > pass1(&volData, PolyVox::Region(Vector3DInt32(62, 62, 62), Vector3DInt32(126, 126, 126)), &tempVolume, PolyVox::Region(Vector3DInt32(62, 62, 62), Vector3DInt32(126, 126, 126)), 3);
//pass1.executeSAT();
//LowPassFilter< RawVolume<MaterialDensityPair44>, LargeVolume<MaterialDensityPair44> > pass2(&tempVolume, PolyVox::Region(Vector3DInt32(62, 62, 62), Vector3DInt32(126, 126, 126)), &volData, PolyVox::Region(Vector3DInt32(62, 62, 62), Vector3DInt32(126, 126, 126)), 3);
//pass2.executeSAT();

Which seems to be the solution to the blocky mesh problem. Uncommenting the code resulted in some compiler errors because LowPassFilter as been updated since the code was originally commented out. Namely, LowPassFilter now requires an accumulation type, which from my deductive work means it needs a Density<T> type. I tried giving it both Density8 and Density16, but in both cases got a compiler error basically saying that it can't convert MaterialDensityPair44 to Density8 or Density16.

Now this isn't really surprising given the comment above talking about implementing a custom MaterialDensityPair that has operators that can handle the conversion, but I've been unable to find an example that shows how that might be done and what considerations should be observed when doing so. If anyone could shed some light on how to properly use LowPassFilter, it'd be much appreciated.

Author:  David Williams [ Wed Aug 21, 2013 6:50 am ]
Post subject:  Re: How to use LowPassFilter

Hi, you're basically right about what's going on. I suggest we start with an example which does work and then go from there. Take a look at the BasicExample, and replace the CubicSurfaceExtractor with the Marching Cubes one:

Code:
//Create a surface extractor. Comment out one of the following two lines to decide which type gets created.
//CubicSurfaceExtractorWithNormals< SimpleVolume<uint8_t> > surfaceExtractor(&volData, volData.getEnclosingRegion(), &mesh);
MarchingCubesSurfaceExtractor< SimpleVolume<uint8_t> > surfaceExtractor(&volData, volData.getEnclosingRegion(), &mesh);


This will give you a jagged mesh as you have seen yourself, but but working with a simple 'uint8_t' voxel type. We can now use the LowPassFilter after we have generated the volume:

Code:
//Create an empty volume and then place a sphere in it
SimpleVolume<uint8_t> volData(PolyVox::Region(Vector3DInt32(0,0,0), Vector3DInt32(63, 63, 63)));
createSphereInVolume(volData, 30);

// Smooth
RawVolume<uint8_t> tempVolume(volData.getEnclosingRegion());
LowPassFilter< SimpleVolume<uint8_t>, RawVolume<uint8_t>, int16_t > pass1(&volData, PolyVox::Region(volData.getEnclosingRegion()), &tempVolume, PolyVox::Region(volData.getEnclosingRegion()), 3);
pass1.executeSAT();
LowPassFilter< RawVolume<uint8_t>, SimpleVolume<uint8_t>, int16_t > pass2(&tempVolume, PolyVox::Region(volData.getEnclosingRegion()), &volData, PolyVox::Region(volData.getEnclosingRegion()), 3);
pass2.executeSAT();


Note that you need a couple more headers:

Code:
#include "PolyVoxCore/CubicSurfaceExtractorWithNormals.h"
#include "PolyVoxCore/LowPassFilter.h"
#include "PolyVoxCore/MarchingCubesSurfaceExtractor.h"
#include "PolyVoxCore/RawVolume.h"
#include "PolyVoxCore/SurfaceMesh.h"
#include "PolyVoxCore/SimpleVolume.h"


I put the full code here for you: http://pastebin.com/EShTTtBR

This should now give you a much smoother volume. It's important to note that another (probably better) approach is to generate smooth volume data in the first place, rather then generating jagged data and then smoothing it. In this example of generating a sphere you could set the voxel value to the inverse of it's distance from the center, rather than thresholding that distance and always writing 0 or 255. But sometimes the LowPassFilter is useful when you can't easily generate such smooth data.

Anyway, what goes wrong when we try to use MaterialDensityPair44? The basic problem is that there is no intuitive way to sum or average materials. If you have two materials identifiers such as 7 and 42, it does not make sense to compute the mathematical average. The resulting value may not be an integer, might not be a valid identifier, and will in no sense be the 'visual average' of the two input materials.

For this reason the MaterialDensityPair44 class does not define mathematical operators such as +, /, etc. You can see these are defined in the Density class (there may be more...):

Code:
// For densities we can supply mathematical operators which behave in an intuitive way.
// In particular the ability to add and subtract densities is important in order to
// apply an averaging filter. The ability to divide by an integer is also needed for
// this same purpose.
Density<Type>& operator+=(const Density<Type>& rhs)
{
   m_uDensity += rhs.m_uDensity;
   return *this;
}

Density<Type>& operator-=(const Density<Type>& rhs)
{
   m_uDensity -= rhs.m_uDensity;
   return *this;
}

Density<Type>& operator/=(uint32_t rhs)
{
   m_uDensity /= rhs;
   return *this;
}


You would need to implement these operators for the MaterialDensityPair44 class, and decide what they should do to material values (ignore them? take the max?). There might be others - my quick test indicated that the LowPassFilter assigns '0' at some point and expects an implicit conversion.

Anyway, I think overall you can see it's a conceptual problem rather than a PolyVox-specific one. But if you decide how it should behave then you should be able to implement the desired behaviour. Let me know if you have more questions about this.

Author:  BlazeCell [ Wed Aug 21, 2013 4:52 pm ]
Post subject:  Re: How to use LowPassFilter

Thanks for the reply.

That makes sense regarding the material smoothing issue.

I tried your code and it almost worked but I was getting compiler errors in LowPassFilter.inl on line 43:
Quote:
error C2589: '(' : illegal token on right side of '::'
error C2059: syntax error : '::'

the code for the line is:
Code:
m_uKernelSize = std::max(m_uKernelSize, static_cast<uint32_t>(3)); //For release builds

I'm using PolyVox version 0.2.1 on Windows 7.

I did some googling and ran across this stackoverflow post:
http://stackoverflow.com/questions/2789481/problem-calling-stdmax
where the top answer had the solution.

I just had to change the line of code to:
Code:
m_uKernelSize = (std::max)(m_uKernelSize, static_cast<uint32_t>(3)); //For release builds

and it worked. This cross-platform gotcha is probably something to be aware of in the future.

So having successfully run this code and viewed the results it is definitely smoother. I tried out your suggestion of making the density values not be so binary and that definitely resulted in smoother results. Though, the inverse distance approach you mentioned ended up with a sphere with half the radius due to the threshold value being at 127 from my observation. When I only adjusted the values of the fringe voxels by replacing
Code:
//If the current voxel is less than 'radius' units from the center then we make it solid.
if(fDistToCenter <= fRadius)
{
   uVoxelValue = 255;
}

with
Code:
//If the current voxel is less than 'radius' units from the center then we make it solid.
if(fDistToCenter <= fRadius)
{
   float fThreshold = fRadius - fDistToCenter;
   
   if (fThreshold < 1.0f)
   {
      //Our new voxel value
      uVoxelValue = (uint8_t)ceil(fThreshold * 255);
   }
   else
   {
      uVoxelValue = 255;
   }
}

I ended up with a much smoother result that retained the same radius.

I have a couple of questions now that I have the smoothing up and working:
    Why do we perform two LowPassFilter passes instead of one?
    What's the difference between LowPassFilter::execute() and LowPassFilter::executeSAT()?
    How might I change the threshold value for a surface extractor if I wanted to customize it?

Author:  David Williams [ Thu Aug 22, 2013 8:01 am ]
Post subject:  Re: How to use LowPassFilter

BlazeCell wrote:
I just had to change the line of code to:
Code:
m_uKernelSize = (std::max)(m_uKernelSize, static_cast<uint32_t>(3)); //For release builds

and it worked. This cross-platform gotcha is probably something to be aware of in the future.


Thanks, but that actual line of code is gone in the develop version of PolyVox and hopefully we've got all the others wrapped in brackets.

BlazeCell wrote:
Why do we perform two LowPassFilter passes instead of one?


You don't have to, it's just habit (and gives more blurring). The LowPassFilter always blurs from a source to a destination (it cannot blur in-place), so if you want the original version blurred then you have to copy the blurred result back over it. In most cases you do want the original version blurred, though in our basic example here we could have just run the surface extractor on the other volume. Doing the second blur pass (with source and destination swapped) was really just an alternative to copying the data back over to the source.

BlazeCell wrote:
What's the difference between LowPassFilter::execute() and LowPassFilter::executeSAT()?


executeSAT() is a version which makes use of Summed Area Tables (see section 8.5) to make the performance of the algorithm independent of the kernel size. Compared to execute(), it will in theory be slower for small kernels and faster for larger kernels. I don't remember how well it was running in practice.

BlazeCell wrote:
How might I change the threshold value for a surface extractor if I wanted to customize it?


You can create an instance of DefaultMarchingCubesController, set the appropriate threshold, and pass that instance to your MarchingCubesSurfaceExtractor.

Author:  BlazeCell [ Thu Aug 22, 2013 7:03 pm ]
Post subject:  Re: How to use LowPassFilter

Thank you for your replies. I feel like I now have a much better grasp on what's going on.

Author:  Turtler [ Fri Nov 22, 2013 10:24 am ]
Post subject:  Re: How to use LowPassFilter

Hey, I have a question about using LowPassFilter on a MaterialDensityPair88 LargeVolume.

Would it be useful to create a temporary LargeVolume<uint8_t> and copy my Density values from my original volume inside. Then running the LowPassFilter from my LargeVolume<uint8_t> to a RawVolume<uint8_t> and afterwards replace my density in my original Volume?

In short:
LargeVolume<MaterialDensity> --> LargeVolume<uint8_t> --LowPassFilter--> RawVolume<uint8_t>
--> LargeVolume<MaterialDensity>

I am worried it would take too much time, first to extract, then using the Filter and then writing back.

Author:  David Williams [ Fri Nov 22, 2013 10:50 pm ]
Post subject:  Re: How to use LowPassFilter

In general I don't think this is a good idea... but the question is why would you want to do this? I assume it's because the MaterialDensityPair88 cannot be used with the LowPassFilter due to the required operators being missing?

The MaterialDensityPair88 is just an example really, so if you have an idea of how you expect the filtering to behave in your particular circumstance then you are welcome to add the required operators (perhaps just ignoring the material values and only operating on the densities). The reason we don't implement this for you is because it's not clear how to filter materials in the general case, but if you have an approach which works for your scenario then you are welcome to use it.

Author:  Turtler [ Sat Nov 23, 2013 8:07 am ]
Post subject:  Re: How to use LowPassFilter

At the moment I am using the material to store my colourvalue at a specific point, so i have a value between 0 and 255 which represents an index on a colourtable with the 8bit - colours (about 216 different ones).
I also try to modify my environment dynamically, so I need to smooth things from time to time. Therefore I just need to change the density value, doing the same with the material would have undesireable effects.
So instead of extracting, filtering and writing back, i should modify the lowpassfilter to my needs?

Author:  David Williams [ Mon Nov 25, 2013 12:29 pm ]
Post subject:  Re: How to use LowPassFilter

The LowPassFilter performs simple averaging by adding the surrounding voxels together and then dividing by the total number of voxels. It may be enough for you to change the definitaion of 'adding' by modifying the operator+=() which is provided for MaterialDensityPair88. It currently reads as follows:

Code:
MaterialDensityPair<Type, NoOfMaterialBits, NoOfDensityBits>& operator+=(const MaterialDensityPair<Type, NoOfMaterialBits, NoOfDensityBits>& rhs)
{
   m_uDensity += rhs.m_uDensity;

   // What should we do with the material? Conceptually the idea of adding materials makes no sense, but for our
   // purposes we consider the 'sum' of two materials to just be the max. At least this way it is commutative.
   m_uMaterial = (std::max)(m_uMaterial, rhs.m_uMaterial);

   return *this;
}


Perhaps if you remove the line which modifies the material then this gives the desired behaviour?

If you can't get it to work in this way then a custom version of the LowPassFilter may indeed be required.

Author:  Turtler [ Tue Nov 26, 2013 9:15 am ]
Post subject:  Re: How to use LowPassFilter

Thanks for the reply.
I removed the line you suggested, but it didnt worked out well. Some of the values disappeared so in the end it smoothed itself to nothing. :)

So I modified the LowPassFilter::execute()
Code:
tSrcVoxel += static_cast<AccumulationType>(srcSampler.peekVoxel1nx1ny1nz().getDensity());
...
tSrcVoxel += static_cast<AccumulationType>(srcSampler.peekVoxel1px1py1pz().getDensity());

tSrcVoxel /= 27;
DstVolumeType::VoxelType newVoxel(srcSampler.peekVoxel0px0py0pz().getMaterial(),tSrcVoxel);
m_pVolDst->setVoxelAt(iSrcX, iSrcY, iSrcZ, voxel);

First I changed that the srcSampler just looks at the Density-value at a point, so tSrcVoxel is just a Density-value and nothing more.
Then I create a new Voxel with the needed parameters and write it to the DestinationVolume.

For my needs it works fine, but I could imagine if you have another VolumeType, where you only want to change the density, it would work too with some small changes.

Page 1 of 2 All times are UTC
Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group
http://www.phpbb.com/