CS 488: Lab 11 – Marble Torus
Welcome to lab, which is a place where you and your peers complete exercises designed to help you learn the ideas discussed in the preceding lectures. It is intended to be a time where you encounter holes in your understanding and talk out loud with your peers and instructor.
Your instructor will bounce around between breakout rooms to check in with you. However, you are encouraged to request assistance if you find your progress blocked.
Designate one of your group to be the host. This individual will be responsible for setting up a Live Share session in Visual Studio Code and submitting your work. No team member should dominate or be expected to carry the group. All members should be writing code and contributing ideas.
Setup
Host, follow these steps:
- Open Visual Studio Code.
- Click File / Open Folder, create a new folder, and open it.
- With the Live Share extension installed, select View / Command Palette, and choose Live Share: Start Collaborative Session.
- Copy the invitation link to your chat.
Non-hosts, join the session.
Task
Your task in this lab is to use Perlin noise to make a torus appear to be made of glossy marble. Follow these steps to complete this lab:
- Render a scene with a torus that can be rotated around its center. Either a trackball or keyboard interface is acceptable. You may already have code to generate a torus, which you are free to use. Here’s mine:
function torus(innerRadius, outerRadius, nlatitudes, nlongitudes) { const radius = outerRadius - innerRadius; const centerX = (innerRadius + outerRadius) * 0.5; const positions = []; const normals = []; const faces = []; for (let ilongitude = 0; ilongitude < nlongitudes; ++ilongitude) { let longitude = ilongitude / nlongitudes * 2 * Math.PI; const iNextLongitude = (ilongitude + 1) % nlongitudes; for (let ilatitude = 0; ilatitude < nlatitudes; ++ilatitude) { let latitude = ilatitude / nlatitudes * 2 * Math.PI; const iNextLatitude = (ilatitude + 1) % nlatitudes; const unrotatedX = radius * Math.cos(latitude) + centerX; const unrotatedY = radius * Math.sin(latitude); const position = new Vector3( unrotatedX * Math.cos(longitude), unrotatedY, unrotatedX * Math.sin(longitude), ); positions.push(position); const normal = new Vector3( Math.cos(latitude) * Math.cos(longitude), Math.sin(latitude), Math.cos(latitude) * Math.sin(longitude), ); normals.push(normal); faces.push([ ilongitude * nlatitudes + ilatitude, ilongitude * nlatitudes + iNextLatitude, iNextLongitude * nlatitudes + ilatitude, ]); faces.push([ ilongitude * nlatitudes + iNextLatitude, iNextLongitude * nlatitudes + iNextLatitude, iNextLongitude * nlatitudes + ilatitude, ]); } } return {positions, faces, normals}; }
- Shade the torus using Blinn-Phong illumination, which we discussed in lecture 11. In the vertex shader, send these values along to the fragment shader: the model space position, the eye space position, and the eye space normal. In the fragment shader, compute the ambient, diffuse, and specular terms. Assume a constant albedo for the moment.
- Add to your project this
Noise
class, which generates 3D noise and employs a slightly faster algorithm than the one we worked through in lecture:Adapt the code to yourexport class Noise { static perlinNoise(p) { const base = new Vector3( Math.floor(p.x) & 255, Math.floor(p.y) & 255, Math.floor(p.z) & 255, ); const apex = new Vector3( (base.x + 1) % 256, (base.y + 1) % 256, (base.z + 1) % 256, ); const fraction = new Vector3( p.x - Math.floor(p.x), p.y - Math.floor(p.y), p.z - Math.floor(p.z), ); const weights = new Vector3( this.fade(fraction.x), this.fade(fraction.y), this.fade(fraction.z), ); const pp = this.permutations; const xyzGradient = pp[pp[pp[base.x] + base.y] + base.z]; const xYzGradient = pp[pp[pp[base.x] + apex.y] + base.z]; const xyZGradient = pp[pp[pp[base.x] + base.y] + apex.z]; const xYZGradient = pp[pp[pp[base.x] + apex.y] + apex.z]; const XyzGradient = pp[pp[pp[apex.x] + base.y] + base.z]; const XYzGradient = pp[pp[pp[apex.x] + apex.y] + base.z]; const XyZGradient = pp[pp[pp[apex.x] + base.y] + apex.z]; const XYZGradient = pp[pp[pp[apex.x] + apex.y] + apex.z]; let xyzDot = this.dotgrad(xyzGradient, fraction.x, fraction.y, fraction.z); let XyzDot = this.dotgrad(XyzGradient, fraction.x - 1, fraction.y, fraction.z); let xYzDot = this.dotgrad(xYzGradient, fraction.x, fraction.y - 1, fraction.z); let XYzDot = this.dotgrad(XYzGradient, fraction.x - 1, fraction.y - 1, fraction.z); let xyZDot = this.dotgrad(xyZGradient, fraction.x, fraction.y, fraction.z - 1); let XyZDot = this.dotgrad(XyZGradient, fraction.x - 1, fraction.y, fraction.z - 1); let xYZDot = this.dotgrad(xYZGradient, fraction.x, fraction.y - 1, fraction.z - 1); let XYZDot = this.dotgrad(XYZGradient, fraction.x - 1, fraction.y - 1, fraction.z - 1); const a = this.lerp(xyzDot, XyzDot, weights.x); const b = this.lerp(xYzDot, XYzDot, weights.x); const c = this.lerp(xyZDot, XyZDot, weights.x); const d = this.lerp(xYZDot, XYZDot, weights.x); const e = this.lerp(a, b, weights.y); const f = this.lerp(c, d, weights.y); const g = this.lerp(e, f, weights.z); return g; } static fractalPerlinNoise(p, layerCount) { let sum = 0; for (let i = 0; i < layerCount; i += 1) { const weight = (1 << i) / ((1 << layerCount) - 1); sum += this.perlinNoise(p.scalarMultiply(1 / (1 << i))) * weight; } return sum; } static dotgrad(hash, x, y, z) { switch (hash & 0xF) { case 0x0: return x + y; case 0x1: return -x + y; case 0x2: return x - y; case 0x3: return -x - y; case 0x4: return x + z; case 0x5: return -x + z; case 0x6: return x - z; case 0x7: return -x - z; case 0x8: return y + z; case 0x9: return -y + z; case 0xA: return y - z; case 0xB: return -y - z; case 0xC: return y + x; case 0xD: return -y + z; case 0xE: return y - x; case 0xF: return -y - z; default: return 0; } } static fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } static lerp(a, b, t) { return a + t * (b - a); } static field3(dimensions, scale, layerCount) { const bytes = new Uint8Array(dimensions.x * dimensions.y * dimensions.z); for (let z = 0; z < dimensions.z; ++z) { for (let y = 0; y < dimensions.y; ++y) { for (let x = 0; x < dimensions.x; ++x) { const p = new Vector3(x * scale.x, y * scale.y, z * scale.z); const value = Noise.fractalPerlinNoise(p, layerCount) * 0.5 + 0.5; bytes[z * dimensions.x * dimensions.y + y * dimensions.x + x] = Math.floor(value * 255); } } } return bytes; } } Noise.permutations = [151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180]; for (let i = 0; i < 256; ++i) { Noise.permutations[i + 256] = Noise.permutations[i]; }
Vector3
class as needed. - Use the
Noise
class to generate a volume of Perlin noise. Get those as aUint8Array
and upload it to a 3D texture with code like this:const bytes = Noise.field3(new Vector3(width, height, depth), new Vector3(0.1, 0.1, 0.1), 3); // ... gl.texImage3D(gl.TEXTURE_3D, 0, gl.R8, width, height, depth, 0, gl.RED, gl.UNSIGNED_BYTE, bytes);
- In the fragment shader, cover the torus in diagonal bands. You can determine a fragment’s diagonality by adding its model space x- and y-coordinates together. Turn the diagonality into the surface’s albedo.
- Make the diagonality oscillate by feeding it to the sine function to compute what we’ll call the grain. Turn the grain into the surface’s albedo. Increase the frequency of the oscillation by multiplying the diagonality by a large number.
- Perturb the oscillating wave by adding a phase shift. First look up the red channel of the noise texture using the model space position for texture coordinates. Weight the noise and add it onto the weighted diagonality.
- Since sine returns values in the range [-1, 1] and each channel of the albedo must be in [0, 1], apply an absolute value to the grain.
- Raise the grain to various powers to achieve a pleasing marble texture. What does raising a number in [0, 1] to a power do? An exponent greater than 1 will make the grain get smaller and therefore darker. An exponent less than 1 will the make the grain get bigger and therefore lighter.
- Submit your
index.js
on Crowdsource. Enter the eIDs for your team members. If you need to make changes after you’ve already submitted, just reload the page and resubmit. If you haven’t finished by the end of the scheduled lab time, you are free to continue working. However, the submission must be made before the end of the day to receive credit.