METHODS:
Basic steps to do noise generated procedural geometry.
Part 1. The density function and rendering the volume density texture
Part 2. The marching cubes algorithm and defining blocks of geometry.
Part 1. The density function and rendering the volume density texture
The beauty of this procedure lies in the manner in which random numbers and mathematics are applied. An aspect that has also motivated me to do this is that it was a nice case study to learn about many facets of DirectX and HLSL.
The procedure uses the marching cubes algorithm to produce trianglestrips that form an isosurface. There are lots of webpages devoted to this method so I won't say too much about it. But to apply this technique you need to define a volume of "density" values that contain isosurfaces. To illustrate this, here is a picture in which the colors represent height. So I can make a second figure in which i draw lines in the height map where the values are the same. I suppose you would call these isolines. Isosurfaces define surfaces where the values are identical. The marching cubes algorithm detects zero crossings and forms triangles where zeros define an isosurface.
So you need to have some method to construct a volume with positive and negative values and the border (zero density) between these values should define a surface. To obtain such a volume from noise we use perlin textures.
This starts by making a matrix of random numbers and calculating smooth interpolations between these numbers. This was originally developed by Ken Perlin to make procedural textures without using to much memory. There are several sites explaining how this works, here is one by Matt Zucker.
Perlin noise starts with a series of random numbers of which a texture is made on the graphics card; (This is based on DirectX11 and HLSL shader programming!!) The code below is just copied out of the example software that is included in the directx download package (DirectX SDK 2010). I have compiled the code in Visual C++ 2010 Express (can also be downloaded for free; playing with directx is a very cheap hobby!!!)
Now you can sample from this texture a HLSL shader program.
Random numbers by themselves are not very nice, but with smooth interpolations... they become perlin noise
There are other ways to interpolate between values, but the sigmoid function (f(x) = 6*x^5 - 15*x^4 + 10*x^3) used here has additional usefull properties.
The Sigmoid function has nice smooth first and second derivatives
The first derivative can be used for estimating a normal, but it can also be seen that it's magnitude depends on the distance from bending points and this property can be used for texturing the geometry later on and this is also why the second derivative is usefull. Because a function is used to interpolate, you can readout this function at any level of detail. In other words you can stretch the red line to any length you like, either making the resulting line have very slowly changing values or relatively fast changing values. And you can add different combinations of lines together to get a complex wavy pattern in which consecutive samples differ gradually as in the next figure.
To understand that I've made perlin noise in 2 dimensional space. This is still not what we want, because although this has 3d structure, we do not as yet have an isosurface of zero crossings. But I can use it to illustrate the next point and that is that if I add a ramp in one direction I obtain a border dividing the sheet in an area with positive values from an area with negative values. This is shown in the lower left figure. If this had been a 3d volume the border would not have been a line but would define a surface that can take any form.
Below a contour is shown from one layer in a 3d texture computed with the shader code shown below. The black pixels are values below zero and the white pixels above one. This has become a density volume texture and will be used in the next steps of the procedure in which I apply the marching cubes algorithm.
float sigmoid( float x)
{
return x*x*x*(x*(x*6.0-15.0)+10.0);
}
float derivative( float x)
{
return 30.0*x*x*(x*(x-2.0)+1.0);
}
float secdiv( float x)
{
return 60.0*x*(x*(2.0*x-3.0)+1.0);
}
struct SDens
{
float4 Dens;
float Amb;
};
SDens noise(float3 fOffset, float frq)
{
float x, y, z, o, p, q, u, v, w, du, dv, dw, ddu, ddv, ddw;
SDens D;
float3 vin;
vin = fOffset/ frq;
o = (vin.x%32); //modulus of 32; to find voxel in 3D tex 32 * 32 * 32 in this way the function can be called with any float3 and always lead to a valid coordinate in the texture.
p = (vin.y%32); //makes number between 0 and 32 because the texture has these dimensions
q = (vin.z%32);
//defines corners of voxel with 4 random values
x = floor(o); //round this value to get a round number between 0 and 31
y = floor(p);
z = floor(q); //that corresponds with start point of 8 adjacent pixels forming a cube in a 3D texture
o = o%1; //remainder is coordinates of vector in cube
p = p%1; //vector in random cube, modulus of 1
q = q%1;
u = sigmoid(o); //cubic spline
v = sigmoid(p);
w = sigmoid(q);
du = derivative(o); //derivative, thanks to iƱigo quilez 2008
dv = derivative(p);
dw = derivative(q);
//now determine offsets to obtain the 8 random values.
uint4 Offsets[8] =
{
uint4( x, y, z, 0), //a
uint4( x + 1, y, z, 0), //b
uint4( x, y + 1, z, 0), //c
uint4( x + 1, y + 1, z, 0), //d
uint4( x, y, z + 1, 0), //e
uint4( x + 1, y, z + 1, 0), //f
uint4( x, y + 1, z + 1, 0), //g
uint4( x + 1, y + 1, z + 1, 0) //h
};
//get values from the random texture
float dp[8];
for(int i = 0; i < 8; i++){
dp[i] = g_txRandVolume.Load(Offsets[i]);
}
//return linear interpolation in 3d space
float k0 = dp[0];
float k1 = dp[1] - dp[0];
float k2 = dp[2] - dp[0];
float k3 = dp[4] - dp[0];
float k4 = dp[0] - dp[1] - dp[2] + dp[3];
float k5 = dp[0] - dp[2] - dp[4] + dp[6];
float k6 = dp[0] - dp[1] - dp[4] + dp[5];
float k7 = - dp[0] + dp[1] + dp[2] - dp[3] + dp[4] - dp[5] - dp[6] + dp[7];
D.Dens.x = du * (k1 + k4*v + k6*w + k7*v*w); //normal(x, y, z)
D.Dens.y = dv * (k2 + k5*w + k4*u + k7*w*u);
D.Dens.z = dw * (k3 + k6*u + k5*v + k7*u*v);
D.Dens.w = ( k0 + k1*u + k2*v + k3*w + k4*u*v + k5*v*w + k6*w*u + k7*u*v*w ) * frq; //density value
D.Amb = (ddu * (k1 + k4*v + k6*w + k7*v*w) + ddv * (k2 + k5*w + k4*u + k7*w*u) + ddw * (k3 + k6*u + k5*v + k7*u*v))/ 3.0;
return D;
}
static const float Frq[7] = { 128, 64.6, 32.7, 16.5, 8.3, 4.1, 1.9 }; //determines the length of the waves
static const float Scl[7] = { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 }; //scaling of each component
float samplenoise(float3 fOffset) //normal and Amb are not necesssary to make a density volume texture
{
float dens = 0;
SDens D;
float3 ws = fOffset;
for(uint i = 0; i < 7; i++){
D = noise(ws, Frq[i]); //obtain density for each wavelength
dens += D.Dens.w * Scl[i]; //scale
}
dens *= 0.3; //this is just to scale the total magnitude of the noise
dens += fOffset.y - 10; //here I add the ramp to the density values with an arbitray offset
return dens;
To make the volume texture you should define a quad and render to a 3d texture using instancing. Use the pixel shader to call the samplenoise function and render the floats to the volume texture. The additional step of the geometry shader is neccessary because a vertexshader cannot define a rendertargetindex. You need this to render to each layer separately in the 3d texture. For this a rendertarget view has been created for the texture (g_pDensVolumeTexRTV).
Directx code;
my DirectX function to render the density volume texture
Render the density volume: HLSL code
Of course you also have to have code that compiles the shaders, and creates the vertex, geometry and pixel shader, in addition to the vertex layout. But this is well documented in the examples included in the Directx documentation.
Part 2. The marching cubes algorithm and defining blocks of geometry.
//create the random numbers
srand( timeGetTime() );
D3D11_SUBRESOURCE_DATA InitData;
InitData.pSysMem = new float[32*32*32];
if( !InitData.pSysMem )
return E_OUTOFMEMORY; InitData.SysMemPitch = 32 * sizeof( float );
InitData.SysMemSlicePitch = 32 * 32 * sizeof( float );
for( int i = 0; i < 32*32*32; i++ )
{
( ( float* )InitData.pSysMem )[i] = float( ( rand() % 10000 ) - 5000 )/5000;
} // Create the texture
D3D11_TEXTURE3D_DESC dstex;
dstex.Width = 32;
dstex.Height = 32;
dstex.Depth = 32;
dstex.MipLevels = 1;
dstex.Format = DXGI_FORMAT_R32_FLOAT;
dstex.Usage = D3D11_USAGE_IMMUTABLE;
dstex.BindFlags = D3D11_BIND_SHADER_RESOURCE;
dstex.CPUAccessFlags = 0;
dstex.MiscFlags = 0;
V_RETURN( pd3dDevice->CreateTexture3D( &dstex, &InitData, &g_pRandomVolumeTexture ) );
SAFE_DELETE_ARRAY( InitData.pSysMem ); // Create a resource view for the 3D volume texture
D3D11_SHADER_RESOURCE_VIEW_DESC SRVDesc;
ZeroMemory( &SRVDesc, sizeof( SRVDesc ) );
SRVDesc.Format = dstex.Format;
SRVDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE3D;
SRVDesc.Texture3D.MostDetailedMip = 0;
SRVDesc.Texture3D.MipLevels = 1;
V_RETURN( pd3dDevice->CreateShaderResourceView( g_pRandomVolumeTexture, &SRVDesc, &g_pRandomVolTexRV ) );