Required Features for Minecraft Only Milestone


Due: Apr 16, 2024

This page requires JavaScript to display formulas correctly.

Minecraft Only Milestone

The assignment is broken down into several features described below. Each feature is worth a number of points and will be graded separately (although later feature may build on and require a working implementation of earlier features). Your code should be correct and free of Javascript errors or other buggy behavior. You will not be graded on software engineering (use design patterns and unit tests if they help you) but unreadable code is less likely to receive partial credit. You should not worry too much about performance or optimizing your code, but egregiously inefficient implementations of any of the features will be penalized.

This project does not come with automated tests; however you can interact with a reference implementation of the assignment here.

Required Features Do Not Sum To 150 Points

You can only earn 130 points by completing the required features. To get full credit for this milestone, implement your choice of additional optional features from the bells and chimes list. You may implement additional optional features for extra credit.

(0 pts) Sanity Check and Build

Your submitted archive should build smoothly when make-minecraft.py is run and should yield a web package that executes without fatal errors on common browsers (Chrome; Firefox; etc). Code that does not build or run will lose anywhere between 5 to 100 points, depending on the time and effort it requires the TA to fix your code. It is your responsibility to thoroughly test your submission.

(60 pts) Terrain Synthesis

You will procedurally generate a voxellized world out of axis-aligned unit cubes. For this project, terrain is a height field over the \(xz\) plane; you do not need to generate caves or overhangs. In principle, terrain extends infinitely in all horizontal directions; obviously you cannot precompute the terrain geometry in the same way you precomputed the Menger sponger, and instead you will need the ability to generate terrain on the fly as the player moves around your world. To do this, cut up the \(xz\) plane into a grid of \(64\times 64\) chunks. At any moment in time, render only the chunk the player is currently standing on, as well as its eight neighbors. When the player crosses a chunk boundary, synthesize new chunks and delete old ones to maintain a \(3\times 3\) chunk region of renderable geometry around the player.

In the starter code, I've written a Chunk class for you that represents one \(64\times 64\) patch of terrain. The starter code creates a single chunk centered at \((0,0)\) and populates it with cubes at random heights. Please refer to this code for an example of how to generate pseudorandom numbers given a seed in TypeScript, and how to draw multiple cubes using instanced rendering (described below).

(20 pts) Value Noise Patch

Implement a helper function which, given a random seed, generates a \(64\times 64\) patch of terrain heights. Use at least three octaves of value noise to produce a correlated noise pattern, with both global (mountains and valleys) and local terrain features. Recall that you can create a low-frequency noise octave by generating a small patch of white noise and then repeatedly upsampling it using bilinear interpolation. Tune the precise parameters of the noise function to your taste; you will receive full credit so long as the patch contains correlated noise of various frequencies with heights in the range \([0, 100]\).

(10 pts) Instanced Cube Rendering

Since this project requires rendering large numbers of cubes with identical geometry, it is an ideal use case for instanced rendering. Instead of sending the GPU the geometry of each individual cube, in instanced rendering you send only one template cube, along with a list of transformations specifying how that one cube should be translated to draw all cubes in the scene. Since you do not need materialize the terrain geometry in RAM, and only need to send a translation vector for each cube rather than a full array of vertices and triangle indices from the CPU to GPU, instanced rendering is a significant performance win. Instanced rendering recently became available as part of the WebGL 2.0 standard, and a tutorial is available here.

I have modified the RenderPass to support instanced rendering. The function drawInstanced(ncopies) tells the GPU to draw a triangle mesh ncopies different times. On its own this is not useful, since all copies will be drawn on top of each other; to pass per-instance data to the GPU, add an instanced vertex attribute buffer using the new function addInstancedAttribute(). Unlike regular attributes, where the vertex shader receives a different attribute value for each different vertex of the object, each vertex in an object instance will receive the same instanced vertex attribute value, with vertices in different object instances receiving different values. For example, the starter code draws 4096 cubes with a single drawInstanced() call. In addition to regular attributes, the starter code passes an array of 4096 per-cube translation vectors in the aOffset instanced vertex attribute buffer. All vertices of the first cube will receive the first translation vector as their aOffset input, all vertices of the second will receive the second, etc.

Turn your patch of height values into a set of cubes drawn by the GPU using instanced rendering. For each chunk grid cell, draw a stack of cubes of the appropriate height. While conceptually each stack extends infinitely downwards, you should only draw cubes that might be visible to the player.

If implemented properly, your application should have no trouble rendering up to nine chunks of terrain in real time on a decently modern machine. Please compare against the reference solution to gauge if your performance is reasonable.

(20 pts) Seamless Chunk Boundaries

Instead of creating only a single chunk of terrain, create a \(3\times 3\) region of chunks around the player's starting location. These chunks should meet seamlessly at their common boundaries (as if the terrain was a single chunk of size \(192\times 192\)). Guaranteeing seamlessness is probably the trickiest engineering challenge in this assignment: I recommend carefully whiteboarding your planned solution with your partner before starting the implementation, to avoid wasting time extensively refactoring a failed approach.

(10 pts) Lazy Chunk Loading

As the player flies around the world, dynamically synthesize and delete chunks of terrain so that there is always a \(3\times 3\) chunk region around the player. Again, chunks should meet seamlessly at their boundaries. Your terrain should also be persistent: if the player visits a chunk, moves far away, and then comes back, the chunk should appear the same on the first and second visits.

The R key should return the player to their starting position and reset the chunks being rendered accordingly.

A minor performance stutter is acceptable when the player crosses a chunk boundary and your code generates new terrain, especially on older machines; again, use the reference solution as a benchmark for reasonable performance.

(40 pts) Procedural Textures

The starter code's shaders draw each cube as solid grey. In the second part of this project, you will spice up the rendering by texturing each cube with Perlin noise.

(20 pts) Perlin Noise Implementation

Implement a perlin() shader function, which takes in a random seed, a position \((u,v)\) in barycentric coordinates on a unit square \(0 \leq u,v \leq 1\), and a grid spacing, and outputs the value of Perlin noise at that position.

Do not copy an Internet implementation (though you are welcome to refer to pseudocode you find online, such as on Wikipedia, for guidance). In particular, you should implement the basic Perlin noise algorithm we discussed in class; do not use precomputed permutation arrays or other performance tricks commonly found in online implementations.

The GPU does not have a built-in pseudorandom number generator. You can use the following snippet, which computes a random number (or random 2D unit vector) at every point in the plane given a seed:

float random (in vec2 pt, in float seed) {
    return fract(sin( (seed + dot(pt.xy, vec2(12.9898,78.233))))*43758.5453123);
    }
    
vec2 unit_vec(in vec2 xy, in float seed) {
    float theta = 6.28318530718*random(xy, seed);
    return vec2(cos(theta), sin(theta));
    }

As mentioned in class, Perlin noise has tangent discontinuities at the grid edges when using bilinear interpolation. To get smooth derivatives on the entire cube face when using a fine Perlin noise grid, use bicubic interpolation instead, by using the following drop-in replacement for GLSL's mix():

float smoothmix(float a0, float a1, float w) {
        return (a1 - a0) * (3.0 - w * 2.0) * w * w + a0;
}

(10 pts) Perlin Noise Textures

Implement at least three different procedural textures, using Perlin noise as a building block. You can combine Perlin noise with standard mathematical functions, as in the sine examples from class, or combine multiple octaves of Perlin noise. To be clear, you should not materialize a literal texture in GPU memory: implement a shader function which computes a color given \(uv\) coordinates and a random seed, and then use that color to shade pixels in a fragment shader (using a mix of diffuse shading and ambient lighting).

(10 pts) Textured Blocks

Combine your procedural textures with your terrain synthesis algorithm to procedurally generate a colorful world. Modify your terrain code to generate not only height, but also a block type, for each grid cell in each chunk. Render each cube differently on the GPU based on its assigned type. Hint: you can either render each block type in a separate render pass, or render all cubes at once, using an instanced vertex attribute to inform the GPU of each block's type.

For full credit, your world should include at least three different block types. No two blocks should be exactly identical. It is OK if two adjacent blocks have a texture seam between them, or if two adjacent faces of a block have a seam.

(30 pts) FPS Controls

Hovering over the 3D world is useful for debugging, but terrain is meant to be walked on, not admired from a distance. Implement rudimentary logic to walk around your landscape.

(20 pts) Collision Detection

Treat the player as a cylinder of radius \(0.4\) that extends two units downward from the camera. Do not allow the user to move in any way that would cause this cylinder to penetrate into a cube: ignore user input during any frame when that input would cause a collision. Hint: you will need to augment your Chunk class with logic for determining the minimum vertical position allowed for the cylinder at a given location on the \(xz\) plane. Just checking the height at the player's current grid cell is not enough, because the cylinder might also overlap one of the cell's eight neighbors. You will need to consider carefully how to fully detect all cases of potential collisions, including at chunk boundaries.

For full credit, the player should not ever "fall through" the world geometry or get stuck partially or wholly inside a cube.

(5 pts) Gravity

Maintain a vertical velocity for the player, and accelerate the player downwards at \(9.8\) units per second squared any time the player's cylindrical body isn't supported by a block underneath. Obviously, your collision detection must prevent gravity from causing penetrations into terrain. Reset velocity to zero whenever the player "lands" on terrain.

(5 pts) Jumping

Implementing jumping whenever the player hits spacebar while standing on solid ground. If you've implemented gravity correctly, jumping should simply be a matter of setting the player's velocity to a positive constant (the reference solution uses \(10\) units per second). Do not allow double-jumps while the player is in the air.

Optional Features

Implement at least 20 points of optional features. You may implement more for extra credit. The list below contains pre-approved optional features (worth five points per 🎐 and ten points per 🔔)

Important Any additional features you attempt must obey the following two cardinal rules:

  1. Include a brief explanation of any scenes/features you would like considered for credit in your README file. This explanation must include instructions for turning on/toggling the extra features. Undocument or poorly-documented features risk receiving no credit.

  2. Some of the following items presuppose a working implementation of the basic, required project features, or of other optional features. You may not receive full credit for a bell or whistle if a dependent feature is buggy or missing.

Approved 🔔 (10 pts each) and 🎐 (5 pts each)