Study of raytracing has been progressing into the second book Ray Tracing: the Next Week, which is a little bit more advanced. This post is going to focus on some notes about Perlin Noise implementation.
Book Implementation Debug
50 shades of perlin noise... pic.twitter.com/IIWrVVsPAV
— ビクター (@viclw17) December 21, 2018
Follow the book I finally got the code working and render the correct images as shown above. One of the most annoying bugs I encountered was at some point the code failed to produce a valid .ppm
image that can be opened by XnViewer (more details about image output see previous post: Raytracing - Image Output). I opened the image with text editor and notice there are lots of RGB values with all 3 elements as -2147483648. This made me wonder if the output of the noise function produced negative values that leads to the negative color value bug. I modified the noise_texture.h
to output the noise value and that proved my speculation:
#ifndef NOISE_TEXTURE_H
#define NOISE_TEXTURE_H
#include "texture.h"
#include "perlin.h"
#include <iostream>
class noise_texture : public texture {
public:
noise_texture() {}
noise_texture(float sc) :scale(sc) {}
virtual vec3 value(float u, float v, const vec3& p) const {
float noise = perlin_noise.noise(scale*p); // bug!
std::cout << noise << std::endl;
return vec3(1.0,1.0,1.0) * noise;
}
perlin perlin_noise;
float scale;
};
#endif
Console output shows the range of noise output seems (-1,1):
Image outputing ...
-0.122155
-0.134105
-0.206453
-0.159719
-0.178172
0.112327
0.0720122
0.0839958
-0.157709
...
Solution is to scale and bias the noise to remap it from (-1,1) to (0,1):
// bug!
float noise = perlin_noise.noise(scale * p);
// fix: scale and bias!
float noise = 0.5 * (1 + perlin_noise.noise(scale * p));
Note: Negative/Invalid color value is one of the most common bugs of rendering… Always check the output range or make sure the output value not overflow the data type and cause strange value!
Building Up Perlin Noise
The implementation in the book is kinda overwhelming for me, so I decided to revisit later (after some emergency reading to refresh my C++ and basic math…)
As recommended by the book I digress a bit to read the post - Building Up Perlin Noise by Andrew Kensler. After a long-time debugging I finally get the code working.
Breakdown
Smoothstep
The original Perlin noise algorithm used a cubic Hermite spline of the form $s(t) = 3t^2 − 2t^3$. This particular function is also sometimes known as smoothstep.
In HLSL and GLSL, smoothstep implements the ${S} _{1}(x)$, the cubic Hermite interpolation after clamping:
\[{smoothstep} (x)=S_{1}(x)={\begin{cases}0&x\leq 0\\3x^{2}-2x^{3}&0\leq x\leq 1\\1&1\leq x\\\end{cases}}\]Again, assuming that the left edge is 0, the right edge is 1, with the transition between edges taking place where $0 ≤ x ≤ 1$.
float f( float t ) {
t = fabsf( t ); // float fabsf( float arg );
return t >= 1.0f ? 0.0f : 1.0f - ( 3.0f - 2.0f * t ) * t * t;
}
Surflet
Detailed explanation see post - Building Up Perlin Noise
float surflet( float x, float y, float grad_x, float grad_y ) {
return f( x ) * f( y ) * ( grad_x * x + grad_y * y );
}
Initialization
static int const size = 256;
static int const mask = size - 1;
int perm[ size ];
float grads_x[ size ], grads_y[ size ];
// produce a random perm array and 2 arrays of random gradients
void init() {
for ( int index = 0; index < size; ++index ) {
int other = rand() % ( index + 1 );
if ( index > other )
perm[ index ] = perm[ other ];
perm[ other ] = index;
grads_x[ index ] = cosf( 2.0f * M_PI * index / size );
grads_y[ index ] = sinf( 2.0f * M_PI * index / size );
}
}
Evaluate Noise
float noise(float x, float y) {
float result = 0.0f;
int cell_x = floorf(x); // provide surflet grids
int cell_y = floorf(y); // provide surflet grids
for (int grid_y = cell_y; grid_y <= cell_y + 1; ++grid_y)
for (int grid_x = cell_x; grid_x <= cell_x + 1; ++grid_x) {
// random hash
int hash = perm[(perm[grid_x & mask] + grid_y) & mask];
// grads_x[hash], grads_y[hash] provide random vector
result += surflet(x - grid_x, y - grid_y, grads_x[hash], grads_y[hash]);
}
return result;
}
Full Implementation and Rendering
PGM Format
More about Netpbm format :
- Portable BitMap P1 .pbm 0–1 (white & black)
- Portable GrayMap P2 .pgm 0–255 (gray scale)
- Portable PixMap P3 .ppm 0–255 (RGB)
Here we are using Portable GrayMap.
#include <fstream>
using namespace std;
#define M_PI 3.1416
static int const size = 256;
static int const mask = size - 1;
int perm[ size ];
float grads_x[ size ], grads_y[ size ];
void init() {
// ...
}
float f(float t) {
// ...
}
float surflet(float x, float y, float grad_x, float grad_y) {
// ...
}
float noise(float x, float y) {
// ...
}
// rendering
int main() {
init();
const int dimension = 1000; // image is 1000x1000 pixels
// output into PGM image
ofstream outfile("render.pgm", ios_base::out);
outfile << "P2\n" << dimension << " " << dimension << "\n255\n";
int lattice = 20; // how many grid cells
int space = dimension / float(lattice);
for (int j = 0; j < dimension; j++) {
float y = (float)j / ((float)space); // cast to float!!!
for (int i = 0; i < dimension; i++) {
float x = (float)i / ((float)space);
float n;
// typical noise
n = noise(x, y); // (-1,1)
n = 0.5 * (n + 1.0); // bias and scale to remap from (-1,1) to (0,1)
// wooden-looking noise
//n = 20 * noise(x, y);
//n = n - floor(n);
// Map the values to the [0, 255] interval for color output
float color = floor(n * 255); // have to be int!
// or
// float color = int(n * 255); // this works the same as floor
outfile << color << " ";
}
outfile << "\n";
}
return 0;
}
Debug
First of all, remember to scale and bias the noise to remap it from [-1,1] to [0,1] to avoid negative color values!
Originally, I forgot to cast the loop indexes into float
when dividing them by space
. This cause the result always be truncated to 0 and produce an image of all black.
Also, at first I was so desperate to see the noise pattern so I just manually scale the noise: n = 128 + 128 * n;
(which is fine) and output the values without casting them into integers outfile << n << " ";
(WRONG!).
This somehow makes the PGM decoder confused with all those float values and produces a noisy image:
More resource
- Solarian Programmer ‘s post Perlin noise in C++11 helped me a lot on debugging and also offered a cool setup to generate wooden-looking noise pattern as noted in the code above.
- Also here is the clearest explanation I found so far:
END