Skip to content

WA3. Bouncing Torus Knot

Statement

Beginning with the example program, create a program that will create a 3-dimensional object. Use the shape of a cube, sphere, or torus knot for this assignment. Your program must display the object within the scene. Your object should be scaled such that it only covers a small portion of the viewing area.

You must animate the object by making it move across the viewing space. When the object reaches the limit of the viewing port, it must change direction (think of it as a ball that is bouncing around in a box). Apply basic color to your object and the color should be changed every time the object encounters an edge of the viewing area.

Your assignment will be assessed (graded) by your peers using the following criteria:

  • Did the assignment output include a 3D object selected from the list? (Yes/No) (Sphere, Cube, or Torus Knot)
  • Was the object animated making it move (In both x and y directions) within the viewing area and simulate bouncing off the walls when the edge of the viewing area was reached? (Yes/No)
  • Was a basic color applied to the object and was the color changed every time the object encountered an edge of the viewing area? (Yes/No)
  • Did the scene employ a light-yellow light source in a fixed position at the upper left corner to illuminate the object and did the illumination demonstrate both light and shadow on the object? (Yes/No)
  • Was the JavaScript / Three.js code well documented (Scale of 1-4 where 1 is no comments and 4 is comprehensive comments)

Answer

Introduction

For this assignment, I chose to create a torus knot. “A (p,q)-torus knot is obtained by looping a string through the hole of a torus p times with q revolutions before joining its ends, where p and q are relatively prime” (Torus Knot, 2024).

The torus knot is animated to start from the origin and move in one plane across the x and y directions within the limits of the viewing area. When the torus knot reaches the edge of the viewing area, it changes direction and continues to move in the opposite direction. The torus gets a random color every time it changes its direction.

Source Code

Here is the source code for the assignment:

const yellowHex = "#ffffaa";

/**
 * Prepare the scene to have shadows.
 * For details see: https://threejs.org/docs/#api/en/renderers/WebGLRenderer.shadowMap
 */
renderer.shadowMap.enabled = true; // enable shadow mapping
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // set the type of shadow mapping

/**
 * Prepares the light simulating daylight.
 * see: https://threejs.org/docs/#api/en/lights/DirectionalLight
 */
directionalLight.color = new THREE.Color(yellowHex); // set a yellow color
const top = canvas.height / 2;
const left = -canvas.width / 2;
directionalLight.position.set(left, top, 1); // set the position of the light to the top left corner
directionalLight.visible = true; // ensure the light is visible
directionalLight.intensity = camera.position.z * 1.5; // enough intensity to be seen from the camera
directionalLight.castShadow = true; // enable shadow casting from this light
directionalLight.lookAt(0, 0, 0); // make the light look at the origin

/**
 * Now, let's create a torus knot.
 * We need to assemble the geometry and material first into a mesh.
 */

// Configuration for the torus knot
// see: https://threejs.org/docs/#api/en/geometries/TorusKnotGeometry
const torusKnotConfig = {
    radius: 35, // radius of the torus knot
    tube: 10, // thickness of tube
    tubularSegments: 100, // number of segments along the tube
    radialSegments: 16, // number of segments around the tube
    p: 2, // number of windings around the torus
    q: 3, // number of windings around the axis of the torus
};

// setup the geometry of the torus knot based on the configuration
const torusKnotGeometry = new THREE.TorusKnotGeometry(torusKnotConfig.radius, torusKnotConfig.tube, torusKnotConfig.tubularSegments, torusKnotConfig.radialSegments, torusKnotConfig.p, torusKnotConfig.q);

// setup the material of the torus knot
const torusKnotMaterial = new THREE.MeshBasicMaterial({
    color: 0xff00ff, // start from a fixed color
    wireframe: shouldShowWireframe(), // show wireframe if option is enabled
    // clipShadows: true,
});

// create the mesh of the torus knot
const torusKnot = new THREE.Mesh(torusKnotGeometry, torusKnotMaterial);
torusKnot.position.set(0, 0, 0); // start from the origin
torusKnot.castShadow = true; // enable shadow casting
torusKnot.receiveShadow = false; // disable shadow receiving
scene.add(torusKnot); // add the torus knot to the scene

let xDirection = 1; // horizontal direction of the torus knot movement: 1 for right, -1 for left
let yDirection = 1; // vertical direction of the torus knot movement: 1 for up, -1 for down
let xSpeed = 5; // horizontal speed of the torus knot
let ySpeed = 5; // vertical speed of the torus knot

let rotationSpeed = 0.01; // speed of the rotation of the torus knot
let rotationDirection = 1; // direction of the rotation of the torus knot: 1 for clockwise, -1 for counter-clockwise

/**
 * Generates a random color of type THREE.Color.
 */
const generateRandomColor = () => {
    const r = THREE.MathUtils.randInt(0, 255);
    const g = THREE.MathUtils.randInt(0, 255);
    const b = THREE.MathUtils.randInt(0, 255);
    return new THREE.Color(`rgb(${r}, ${g}, ${b})`);
};

/**
 * We will add a plane to the scene to simulate the ground.
 * This just makes the lights and shadows more visible.
 */
const planeGeometry = new THREE.PlaneGeometry(window.innerWidth, window.innerHeight, 32, 32);
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.receiveShadow = true; // enable shadow receiving
plane.castShadow = false; // disable shadow casting
plane.position.set(0, 0, 1); // place the plane just above the origin
scene.add(plane); // add the plane to the scene

const animate = () => {
    /**
     * Let's determine the coordinates of the edges of view first,
     * then we can compare with the current position of the torus knot
     */
    const torusKnotLength = 2 * (torusKnotConfig.radius + torusKnotConfig.tube); // approximation  of the length of the torus knot
    const viewEdges = {
        top: -(canvas.height / 2 - torusKnotLength), // screen top x coordinate
        bottom: canvas.height / 2 - torusKnotLength, // screen bottom x coordinate
        left: -(canvas.width / 2 - torusKnotLength), // screen left y coordinate
        right: canvas.width / 2 - torusKnotLength, // screen right y coordinate
    };

    // find the coordinate of the current position of the torus knot
    const torusKnotEdgeX = torusKnot.position.x; // x coordinate of the torus knot
    const torusKnotEdgeY = torusKnot.position.y; // y coordinate of the torus knot

    /**
     * The edges of the view are the boundaries of x,y coordinates that the torus knot can move.
     * After hitting an edge, the torus knot will change its direction and color.
     * E.g. When the x of the torus knot is greater than the right edge of the view, the torus should go left.
     */

    // check the right edge of the view
    if (torusKnotEdgeX >= viewEdges.right) {
        xDirection = -1; // change the direction to left
        torusKnotMaterial.color = generateRandomColor();
    }

    // check the left edge of the view
    if (torusKnotEdgeX <= viewEdges.left) {
        xDirection = 1; // change the direction to right
        torusKnotMaterial.color = generateRandomColor();
    }

    // check the bottom edge of the view
    if (torusKnotEdgeY >= viewEdges.bottom) {
        yDirection = -1; // change the direction to up
        torusKnotMaterial.color = generateRandomColor();
    }

    // check the top edge of the view
    if (torusKnotEdgeY <= viewEdges.top) {
        yDirection = 1; // change the direction to down
        torusKnotMaterial.color = generateRandomColor();
    }

    /**
     * Update the position and rotation of the torus knot.
     * This is not required it makes things more interesting.
     */
    torusKnot.rotation.x += rotationSpeed * rotationDirection;
    torusKnot.rotation.y += rotationSpeed * rotationDirection;
    torusKnot.rotation.z += rotationSpeed * rotationDirection;

    /**
     * Update the position of the torus knot based on the speed and direction.
     */
    torusKnot.position.x += xSpeed * xDirection;
    torusKnot.position.y += ySpeed * yDirection;

    render(); // re-render the scene
    requestAnimationFrame(animate); // prepare for the next frame
};

animate();

Explanation

The main idea is to find the coordinates of the edges of the view and compare them with the current position of the torus knot. After hitting an edge, the torus knot changes its direction and color. For example, when the x of the torus knot is greater than the right edge of the view, the torus should go left.

The new color is generated using the generateRandomColor function, which generate 3 random integers between 0 and 255 and supplies them to the THREE.Color constructor as values for red, green, and blue channels. To make the shadows more visible, a plane is added to the scene to simulate the ground, and then the receiveShadow and castShadow properties are set accordingly.

Screenshots

Below are some screenshots of the torus knot bouncing around the viewing area:

Torus Knot Bouncing 2


Torus Knot Bouncing 3


Torus Knot Bouncing 4


Torus Knot Bouncing 5


Conclusion

See the source code and the live demo for more details:

References