The initial lab template can be accessed in the Lab 6
link on our discussion board. In Parts 1 - 4 of this lab, we will implement a procedural texturing technique called Perlin noise. Part 5 is optional and will provide more practice with the cut-and-paste texturing technique we implemented in class.
The Perlin noise generation algorithm consists of two main ingredients: (1) a background grid (or series of background grids) in which each node of the background grid(s) stores a random 2d unit vector and (2) a method to calculate noise from these random vectors.
The first ingredient consists of creating a background grid with a random 2d unit vector defined at each node of the grid. A grid is a structured array of square cells, and each corner of a cell is a point (or node) in the grid. Let's say there are ngx
cells in the x-direction and ngy
cells in the y-direction (ngx * ngy
cells total). This means there are ngx + 1
points in the x-direction and ngy + 1
points in the y-direction ((ngx + 1) * (ngy + 1)
nodes total). Here is a 4x4 grid (the dots correspond to the nodes/points):
We will define a random 2d unit vector at each node of the grid. We can do this by generating a random angle Math.random()
by Math.cos
and Math.sin
to get the x- and y-components of the vector. Create the vector using the glMatrix
vec2.fromValues
function with (randomGradient
function.
We will represent our grid as a 2d array of 2d vectors:
let grid = new Array(ngx + 1);
for (let i = 0; i <= ngx; i++) {
grid[i] = new Array(ngy + 1);
for (let j = 0; j <= ngy; j++) {
grid[i][j] = randomGradient();
}
}
Note that this 2d array is set up so that we can index the grid vectors similar to how point coordinates are represented. So, the gradient at the grid node with indices (gx, gy)
can be accessed using grid[gx][gy]
.
There are actually two different grids here (and maybe more in the last section of the lab). The first grid is our usual grid of pixels with, say, npx
pixels along the width and npy
pixels along the height of the image. The second grid is overlaid on our image grid. In the image below, each pixel is associated with a blue dot in a 24 x 16 image. The red lines show the boundaries of the cells of the 6 x 4 background grid, and the black dots are the nodes of this background grid. The black dots represent where the random 2d gradients are stored.
We need to figure out the coordinates of each pixel in the "grid system" since that's where the gradients are stored. For pixel (i, j)
, the question we want to answer is:
How many grid cells do we need to traverse in the horizontal direction (x
), and how many in the vertical direction (y
) in order to get to a particular pixel?
In our example above, note that there are 4 full pixels along the width and height of each grid cell. This can be more generally calculated as lx = npx / ngx
and ly = npy / ngy
. Then the answer to the question above is:
x = (i + 0.5) / lx
and y = (j + 0.5) / ly
.
Why the 0.5? It doesn't really matter, but notice the blue dots are at the center of every pixel. Note that these will be floating-point values. You can also assume the grid has the
The next step in the Perlin noise algorithm consists of calculating a weight (or intensity) for every pixel. This weight will be used to interpolate between two colors. Interpolating between white and a blue-ish color will allow us to make clouds.
Let's zoom into the yellow square (from the grid) in the picture above and focus on the green pixel (from the canvas). Every pixel will lie within some grid cell and we will use the four gradient vectors (at the black dots) of the enclosing grid cell to calculate the weight.
We can retrieve these four vectors by knowing the lower-left corner of the grid cell the pixel is in (technically this is the upper-left corner since the grid is aligned with the canvas). Recall that x
and y
represent how many grid cells (including fractions) to traverse in the horizontal and vertical directions. Therefore, we can round these down to the nearest integer to get the grid indices of the lower left corner. Letting (ax, ay)
denote this corner, this grid point is indexed by:
const ax = Math.floor(x);
const ay = Math.floor(y);
So the gradient at ga = grid[ax][ay]
.
Part 3A: Using ax
and ay
, determine the indices of the grid nodes ax
or ay
.
Next, Perlin's algorithm says we need to calculate the dot product between the gradient (ax, ay)
and the offset vector from (ax, ay)
to
Part 3B: Calculate da
, db
, dc
and dd
.
Finally, we'll interpolate the four scalars (da
, db
, dc
, dd
) along the x-direction (this part is already implemented):
const wab = interpolate(da, db, x - ax);
const wcd = interpolate(dc, dd, x - ax);
and then along the y-direction:
const weight = interpolate(wab, wcd, y - ay);
The weight
is what we will return
in the getPerlinWeight
function.
Note that the interpolate
function can be a simple lerp
:
const interpolate = function(a, b, t) {
return a + (b - a) * t;
}
Using the resulting weight (computed for every pixel) to interpolate between blue and white on a 4x4 grid should yield the bottom-left image below (subject to variability because of Math.random
). Notice that the result looks a bit blocky.
Part 3C: use a slightly better interpolation technique in the interpolate
function. You can implement either cubic Hermite interpolation:
Instead of using a single grid, we can use multiple grids to create Perlin noise. Where you see Part 4A
in the template, please initialize multiple grids (at least ngx
or ngy
values in the draw
method).
Then, to calculate the weight at a particular pixel (Part 4B
), loop through all the grids, calculate the weight contributed by each grid and attenuate the weight by some "amplitude". The final weight we use to interpolate the two colors (white and blue) at a pixel can be expressed as:
where getPerlinWeight
for grid
color = vec3.lerp(vec3.create(), color0, color1, wp);
For example, the values for
When we add up the corresponding
Please feel free to be creative with the colors here! With a total of 6 grids, here is what I see using the above scheme (again, the weights are halved with each grid):
In your final submission, please use at least 4 grids to calculate the color at any given pixel. I recommend always using powers of 2 for your image & grid sizes.
To really make this "cloudy with a meatball" (inspired by the movie), add a sphere in the scene and render the sphere with the texture image provided by the mars.jpg
image. Adding specular highlights will kind of make Mars look like a meatball. If you do this, I would recommend setting up the sphere (and corresponding texture) similar to what we did in class.
The initial submission for the lab is due on Thursday 4/10 at 11:59pm EDT. Remember to complete the code for all required parts of the lab: Parts 1, 2, 3A, 3B, 3C, 4A, 4B.
Please see the Setup page for instructions on how to commit and push your work to your GitHub repository and then submit your repository to Gradescope (in the Lab 6 assignment). I will then provide feedback within 1 week so you can edit your submission.