CPSC 424 Computer Graphics Fall 2025

CPSC 424 Lab 6: Textures

Due: Tue 10/14 at the start of lab

This lab deals with textures.

Successful completion of this lab means that you:

Collaboration and Use of AI

This is an individual lab. You may get technical help from others, but the effort and ideas that go into producing solutions should be yours.

You may use AI as outlined in the Use of AI policy — Copilot's inline coding suggestions, explain, fix, review and comment features but not code generation from English prompts. Also:

Handin

Hand in your work by copying your ~/cs424/lab6 folder into your handin folder (/classes/cs424/handin/username, where username is your username).

Check that the result is that your files are contained in /classes/cs424/handin/username/lab6 — if not, fix it!


Preliminaries

Setup

Make sure that your lab6 directory is named exactly like that and is at the same level in your workspace directory. This is important so that the relative path names used to access common files remain the same so your program doesn't break when you hand it in.

Provided Code and Files

The textures directory you copied contains a number of image textures. Some are full color, others are grayscale. For the most part, the grayscale images are 512x512 and the full color images are not.

The lib directory contains one new file:

Reference

Your best reference sources are the slides from class (which pull out and organize the key points), the examples from class (which put all the pieces together), and the textbook.


Exercises

Image Textures

Work with lab6a.html for this section.

The provided lab6a.html supports applying image textures to shapes with supplied texture coordinates.

Blend Modes

The provided code supports getting pixel colors purely from lighting (and the object's material) and purely from the texture. Three other options (mix, replace, modulate) were discussed in class.

*** BUG FIX!!! *** In the fragment shader's main, the u_texmode values for replace and modulate were switched — replace should be 3, modulate should be 4.

Texture Transforms

Texture transforms are used to transform the texture coordinates before sampling the texture. They allow for scaling, rotating, and adjusting the alignment of the texture on the surface without having to define a new set of texture coordinates.

Optionally, feel free to fill in the TODO case in getTexTransform to specify your own texture transform to apply.

Assigning Texture Coordinates

Texture coordinates can be assigned manually as part of the model geometry. In this part, you'll add a new shape — the prism shown below — with manually-assigned texture coordinates.

The geometry for the teapot and other shapes is defined as a JavaScript object in the following form:

	{
	  vertexPositions: ...,
	  vertexNormals: ...,
	  vertexTextureCoords: ...,
	  indices: ...
	}
      

The ... is a Float32Array for everything but indices, which is a Uint16Array. See the teapot model for an example.

Note that these are defined as triangular meshes — every face is a triangle, so the faces can be given as a 1D list of vertices with every three vertices taken to be a face.

You'll need to define all four properties, but you can define vertexNormals as an empty Float32Array (new Float32Array([])). Lighting won't work, but texture-only should. (Leaving out the vertexNormals property entirely will likely break everything.)

For the prism, wrap the texture once around the sides, so that 1/3 of the texture is used on each side and the edges line up. You can use any triangle from the texture for the top and bottom of the prism; as a challenge (and for some extra credit), make the triangle line up with one of the edges so that it looks like a flap of texture folded over from one side of the prism and also make the scaling of the texture on the sides of the prism match up with the scaling on the ends.

Keep in mind that while the general goal of the indexed face set representation is to not repeat vertices that are shared by multiple faces, that assumes that all of the properties of the vertex — coordinates but also normals, texture coordinates, etc — are shared by every occurrence of that vertex. If any of your vertices have different texture coordinates (or normals) in different faces, you'll need to repeat them.

If you are defining vertex normals (optional), also keep in mind that the prism is a polyhedron, not a polygonal mesh approximating a smooth surface. You'll want flat shading and thus polygon normals rather than vertex normals.

Multiple Textures

(There's no part 6b.)

Generating Texture Coordinates

Work with lab6c.html for this section.

The provided lab6c.html uses the texture coordinates defined as part of the model. Your task is to implement two ways of generating texture coordinates — projection onto the xy plane and the "convert point to shape coordinates" approach using a sphere as the shape.

Technical notes:

Projecting onto the xy plane means mapping the x and y components of the OC (x,y,z) point to u and v, respectively. Keep in mind the technical note above — the OC values range from -0.5 to 0.5 while u and v should be in the range 0 to 1.

"Shape coordinates — sphere" means treating the OC (x,y,z) point as if it is a point on a sphere and applying the shrinkwrap method to determine (u,v). The computation:

  u = atan(y,x)/2π + 0.5
  v = asin(z/r)/π + 0.5

where r is the radius of the sphere through (x,y,z) — this is also the distance from (x,y,z) to the origin and you can use the GLSL function distance to compute r. GLSL also has functions atan and asin but it does not define a constant for π — you'll have to use 3.1415927.

Optionally implement the other three methods (projection — cube, cube map shape — normal, and sphere map shape — normal). The slides from class provide a definition for what each of these are, but you'll have to work out the math for yourself. (This gets trickier for the map shape methods since you'll need to compute ray-cube and ray-sphere intersections.)

Procedural Textures

Work with lab6d.html for this section.

The provided lab6d.html supports applying procedural textures to shapes.

The goal of this section is to gain a (very) introductory understanding of how one can start to build up procedural textures to achieve various effects. You'll modify aspects of the 2D and 3D checkerboard and marble textures from class to better understand how they work, and then create two new textures (stripes and wood).

Technical notes:

Do the following steps and think about the questions posed, but you don't need to write down answers or turn in anything.

Stripes alternate between two colors. This can be viewed as a blend of two colors where the blend amount is either 0 or 1, which gives a design strategy for a stripes texture — figure out a function that yields blend amounts that alternate between 0 and 1.

The stripes2D function in the fragment shader contains a skeleton implementation for a 2D stripes texture which sets up this strategy. Your task is to replace the hardcoded 0.5 value for blend to produce stripes as noted by the TODO comment — follow the steps below. Feel free to change the two colors defined and/or n.

  • Start with identifying a basic function whose value is either 0 or 1 — this is the step function. (Review the slide about gradients from class.) The following uses the x coordinate of the texture to compute the blend, mapping coordinates less than 0.5 to 0 and coordinates greater than 0.5 to 1:

      float value = texcoords.x;
      float blend = step(0.5,value);
    

    Since the texture coordinates are assumed to be between 0 and 1, this will result in an even split of colors. Try changing the 0.5 to other values between 0 and 1 to understand how step works.

  • To repeat the stripes, subdivide the 0 to 1 range into smaller strips and consider where the texture coordinate falls within a strip:

      float value = mod(texcoords.x,1.0/n)*n;
    

    1.0/n is the width of one stripe when the 0 to 1 range is divided into n strips, mod(texcoords.x,1.0/n) determines the remainder when as many full-width strips as possible are subtracted from texcoords.x, and the final *n scales that remainder (a value between 0 and 1.0/n) to a value between 0 and 1.

  • The abruptness of the step function — the value is either 0 or 1 — results in jaggies along the edges of the stripes. Recall from class (the gradients slide) that the smoothstep function provides for a softer transition. Use that instead of step:

      float value = mod(texcoords.x,1.0/n)*n;
      float blend = smoothstep(0.5-a,0.5+a,value);
    

    2a is the width of the transition between 0 and 1. Pick a value that yields a pleasing result by softening the jaggies without being too blurry.

  • This only smooths the transition in the middle of each stripe — there's still a sharp transition between one stripe and the next. Fix this by shifting the middle of each stripe: compute the distance between value (in the range 0 to 1) and the middle of the stripe (at 0.5), the scale the result (in the range -0.5 to 0.5) to be in the range 0 to 1.

        float dist = value-0.5;
        float blend = smoothstep(0.5-a,0.5+a,abs(dist*2.0));
    

A wood texture also involves gradients between two colors. The wood3D function in the fragment shader contains a skeleton implementation for a blending of two colors, similar to the starting point for the stripes. As with stripes, your task is to replace the hardcoded 0.5 value for blend to produce a wood grain texture as noted by the TODO comment — follow the steps below. Feel free to change the two colors defined, though keeping some sort of shades of brown will help the texture look more like wood.

  • Wood grain comes from tree rings and tree rings are circular. A strategy for circles is to use the distance from a center point rather than using the texture coordinates directly:

      float dist = distance(objcoords.xy,vec2(0.5));	
      float value = dist*2.0;
      float blend = smoothstep(0.5-a,0.5+a,value);
    

    Choose a larger value for a than you did for the stripes texture — a wider and more noticeable transition between light and dark colors is desirable.

    (0.5,0.5) is the center point because the texture coordinates are assumed to be in the range 0-1 — using distance(objcoords.xy,vec2(0.5)) centers the tree rings in the xy plane.

  • Tree rings are circular stripes — we need repetition. Use the same mod strategy as with the stripes texture:

      float value = mod(dist*2.0,1.0/n)*n;
    

    Also add a definition for n.

    Note that having a smoother color transition on one side and the abrupt next-stripe transition on the other side of the stripe looks good with wood grain so there's no need to shift the middle as with the 2D stripe texture.

  • Why use objcoords.xy to compute dist? Try changing it to objcoords.xz or objcoords.yz to see the effect. (Then change it back.)

  • Tree rings are not perfectly regular. Time for some noise! To warp the rings, add noise to dist:

        dist += amt*snoise(objcoords.xy);
      

    Pick a value for amt for a pleasing effect — try both large and small values and adjust from there. Remember that snoise is not a GLSL function; you can find its definition earlier in the fragment shader.

  • Both dist and the added noise are based on objcoords.xy. What happens if they are different? Try changing the value used for snoise to objcoords.xz or objcoords.yz to see the effect. (Then change it back.)

  • Tree rings also aren't perfectly uniform up and down the trunk — there should be some variation according to the z coordinate as well, but perhaps not as much as the variation of the rings within the xy plane. To involve z, use objcoords.xyz with the noise function, and to have a lesser perturbation, give that contribution a lower weight.

        dist += amt*snoise(objcoords.xy)+amt2*snoise(objcoords.xyz);
      

There are several optional (extra credit!) extensions possible —