\( \newcommand{\vecIII}[3]{\left[\begin{array}{c} #1\\#2\\#3 \end{array}\right]} \newcommand{\vecIV}[4]{\left[\begin{array}{c} #1\\#2\\#3\\#4 \end{array}\right]} \newcommand{\Choose}[2]{ { { #1 }\choose{ #2 } } } \newcommand{\vecII}[2]{\left[\begin{array}{c} #1\\#2 \end{array}\right]} \newcommand{\vecIII}[3]{\left[\begin{array}{c} #1\\#2\\#3 \end{array}\right]} \newcommand{\vecIV}[4]{\left[\begin{array}{c} #1\\#2\\#3\\#4 \end{array}\right]} \newcommand{\matIIxII}[4]{\left[ \begin{array}{cc} #1 & #2 \\ #3 & #4 \end{array}\right]} \newcommand{\matIIIxIII}[9]{\left[ \begin{array}{ccc} #1 & #2 & #3 \\ #4 & #5 & #6 \\ #7 & #8 & #9 \end{array}\right]} \)

Texture Mapping 1

Today, we'll learn to make much more realistic looking colors.

Plan

  • Announcements
  • Conclude Material and Lighting
  • Vector Geometry
  • Quiz Questions
  • Texture Mapping recap
  • Asynchronous Programming
  • Exercise: Mapping onto a Plane

Announcements

  • Grading Status
  • Don't start on Textured Barn yet; I need to double-check some things
  • A look ahead: creative scene and project

Material and Lighting

old notes on material and lighting

More Info about Lighting

There have been some changes since I last taught this course.

  • Now, light intensity falls off much faster with distance, so you will need a higher intensity value.
  • You can set light.distance=0 to undo the falloff with distance, though you may still need
  • Spotlights may need light.decay=0; alternatively, this is the 6th argument of the Spotlight constructor.
    //create the spotlight
    let spotLight = new THREE.SpotLight( sceneParams.spotlightColor,
                                         sceneParams.spotlightIntensity,
                                         sceneParams.spotlightDistance,
                                         sceneParams.spotlightAngle,
                                         sceneParams.spotlightPenumbra,
                                         sceneParams.spotlightDecay );

That last argument should probably be zero, not the default (2).

Let's explore an updated Spotlight tutor. We will also learn about SpotLightHelper

threejs/demos/MaterialLighting/spotlight-tutor/

Play with:

  • spotlightIntensity
  • spotlightDistance
  • spotlightPenumbra
  • spotlightDecay (default is 2, but 0 often works better for us)

SpotLight Helper

  • Draws lines radiating from the location of your spotlight in the direction it's aimed, to help visualize where it's pointed.
  • Default color is same as the light, which is sometimes okay and sometimes not.

  • There is also PointLightHelper and DirectionalLightHelper

Exercise on Luxo Lamps

Let's add spotlights to our Luxo Lamp family.

You can copy the luxo family starter code like this:

cd ~/public_html/cs307/
cp -r /home/cs307/public_html/lectures/material-lighting1/lamp-start/ litlamps

Here's what the starter looks like: lamp-start

The starting code has only one spotlight, aimed the wrong way, and not shaped right.

lamp.addConeLight(scene,
                  new THREE.Vector3(0,15,0),
                  new THREE.Vector3(0,0,0),
                  true);

Have a scene with two spotlights. You'll need to do the following:

  • convert the materials to Phong
  • add the spotlights to the scene
    • at the location of the "light bulbs"
    • pointed in the same direction

Geometry

Main ideas

  • dot product is useful and quick to compute
  • dot product with self yields the length of the vector
  • dividing through by length yields a normalized vector (unit length)
  • dot product of normalized vectors is the cosine of the angle between them

Cross Product

  • useful to find a vector perpendicular to both of two vectors
  • $ \vec{n} = \vec{v} \times \vec{w} $
  • right hand rule, two ways:
    1. fingers sweep the angle between them: thumb points in direction of cross product
    2. finger alighnment:
      • index finger in direction of $ \vec{u} $
      • middle finger in direction of $ \vec{w} $
      • thumb in direction of $ \vec{n} $

Cross product is useful to find surface normals

  • find two vectors lying in the plane, and take their cross product
  • if you have 3 vertices ABC (in CCW order) lying in the plane:
    • let $ \vec{v} = B - A $
    • let $ \vec{w} = C - A $
    • $ \vec{n} = \vec{v} \times \vec{w} $

Quiz Questions

quiz questions

Texture Mapping

old notes on texture mapping

Let's explore two demos. First, a simple plane with texture on it:

flower plane

Next, a box with a different texture on each side:

box

Try the keyboard callbacks:

  1. faces
  2. UV coordinates
  3. die faces

Asynchronous Programming

Loading textures can require asynchronous programming, which is new to most of us.

  • I/O takes time, and the browser doesn't want to freeze waiting for I/O to complete
  • There are several strategies to deal with that:
    • ignore it, and assume that a later render will fix things up. Threejs does this a lot.
    • callbacks
    • promises
    • async/await

Ignore It

Threejs TextureLoader documentation

Surprisingly (at least to me), it works to treat the I/O as if it were synchronous.

The TextureLoader.load() is asynchronous but does not block execution. Instead, it immediately returns a texture object that Three.js initializes and uses, even though the image data is still loading in the background. What Happens Internally?

  • loader.load(url) is called. This:
    • Creates a new THREE.Texture object.
    • Starts loading the image asynchronously.
    • Returns the texture object immediately.
  • You assign this texture to a MeshBasicMaterial, which is then assigned to a Mesh and added to the scene.
  • The initial render() happens with an empty texture, because the image hasn't loaded yet.
  • Once the image fully loads, Three.js automatically updates the texture on the next render frame.

Because of the camera we are using, there's a render loop that causes the scene to be continuously re-rendered. Once all the textures are loaded, they'll be displayed.

However, there may be times when you want to be more careful and only render once the texture is loaded.

Callbacks

Callbacks is easy and oldest. It's what the Threejs example shows after the "ignore it" option:

Threejs TextureLoader documentation again

// instantiate a loader
const loader = new THREE.TextureLoader();

// load a resource
loader.load(
    // resource URL
    'textures/land_ocean_ice_cloud_2048.jpg',

    // onLoad callback
    function ( texture ) {
        // in this example we create the material when the texture is loaded
        const material = new THREE.MeshBasicMaterial( {
            color: 0xffffff,
            map: texture
         } );
         // use the material here
    },
);
// texture isn't loaded yet here, and it's not in scope anyhow, 
// so you can't use the material here.

The callback approach works really well if you are only loading one texture. If you are loading 6 however, you run into difficulties.

  • You can load first one, then the next, but the code becomes deeply embedded. This is described as callback hell

A solution to callback hell is the use of promises and async/await

Promises

The Three.js loader.loadAsync() function starts the texture loading, but returns a promise.

const p = loader.loadAsync(url);
p.then(function (texure) {
        // in this example we create the material when the texture is loaded
        const material = new THREE.MeshBasicMaterial( {
            color: 0xffffff,
            map: texture
         } );
         // use the material here
       });

The nice thing about promises is that you can collect them and wait for them all to complete:

const promises = urls.map( url => loader.loadAsync(url) );
Promise.all(promises).then( function(textures) {
    // use the array of fulfilled promises, namely textures
    ...
    });

Async/Await

Promises are easy enough, but async and await were invented to give the illusion of more serial coding.

const promises = urls.map( url => loader.loadAsync(url) );
const textures = await Promise.all(promises);
// use the array of fulfilled promises, namely textures
...

Here's some example code (from TextureMapping/box/textureBox.js

export async function box6 (geom, filenameArray) {
    if( ! geom instanceof THREE.BoxGeometry) {
        throw new Error('first argument must be box geometry');
    }
    if(filenameArray.length != 6) {
        throw new Error('argument must be an array of 6 filenames');
    }
    const loader = new THREE.TextureLoader();
    // Note that we use loadAsync() here, not load().
    // loadAsync() returns a promise
    const promises = filenameArray.map(a => loader.loadAsync(a));
    console.log(promises[0], promises[5]);
    const textures = await Promise.all(promises);
    console.log('all textures loaded', textures);
    // palette of possible textures to use for sides
    const materials = textures
          .map( t => new THREE.MeshBasicMaterial({color: 0xffffff, map: t}));
    var boxMesh = new THREE.Mesh(geom, materials);
    return boxMesh;
}

Demo

Here's a demonstration of what a page might look like if we

  • load six images (all potentially visible)
  • don't do any kind of callback/
  • don't have an animation loop

threejs/demos/TextureMapping/siximages/

Here's the code:

// Function adds the textured box to the scene once the textures are loaded.
const planeGeom = new THREE.PlaneGeometry(1, 1);

const faceFiles = ["buffy.gif",
                   "willow.gif",
                   "xander.gif",
                   "angel.gif",
                   "giles.gif",
                   "dawn.gif"];

const loader = new THREE.TextureLoader();
for(const i in faceFiles) {
    const ff = faceFiles[i];
    // append a random number to the URL to ensure that the image hasn't been cached
    const rnd = Math.floor(Math.random()*10000);
    const texture = loader.load("../../../images/"+ff+"?"+rnd);
    const mat = new THREE.MeshBasicMaterial({color: 0xffffff, map: texture});
    const mesh = new THREE.Mesh( planeGeom, mat );
    scene.add(mesh);
    mesh.position.x = i;
    console.log('added ', ff);
}
// This might be before the images are all loaded
TW.render();

Advice

For the purposes of this course, it's fine to use the ignore it approach. It simplifies the coding and has no appreciable impact on the graphics application.

If you are using your own camera, you won't have a render loop, and the ignore it approach won't work. You will need to use one of the other techniques.

Furthermore, it's important to understand asynchronous coding, which is increasingly important in modern practice.

Exercise

The key tricky part of using textures is getting the image onto the same server as the JS file. You'll need to:

  • download a picture to your laptop
  • scp the file to the server (I usually use my home directory)
  • login to the server
  • mv the file to the same folder as the js file
  • probably use opendir to ensure that the image file is readable

I suggest you do that. Start by downloading the image, then on your laptop, open a terminal and do:

[laptop] scp IMAGEFILE ACCT@cs:

replacing the IMAGEFILE with the name of the file you downloaded and ACCT with your username. Don't forget the colon at the end! Don't type [laptop]; that's the prompt, to remind you it should be on your laptop.

Then, on the server, copy the the plane demo:

cd ~/public_html/cs307/
cp -r ~cs307/demos/TextureMapping/flowerPlane texplane
mv ~/IMAGEFILENAME texplane
opendir texplane
cd texplane

Get that to work, and you'll be in good shape.

Summary

  • Lights work mostly as expected, but
    • turn up the intensity
    • set decay to zero
  • Geometry
    • dot products are quick to compute and give us the cosine of the angle, iff the vectors are normalized
    • cross products help us find normal vectors
  • Texture Mapping
    • load a file and "paint" it onto a surface
    • sometimes requires thinking about asynchronous programming

Next Time

More texture mapping

  • texture coordinates
  • repeating textures
  • lighting and textures