8/13
6. Structuring code
Use of functions
As in any programming language, it is essential that you split large code structure into functions. It will ease readability of your code, its debug, maintenance.
The following code proposes another 3D scene with four objects (cube, sphere, cylinder, and a plane) and the use of shadows.
"use strict";
// ************************************* //
// Functions definition
// ************************************* //
// Create an empty scene with a camera, spotlight, and a renderer
function initEmptyScene(sceneElements) {
// ************************** //
// Create the 3D scene
// ************************** //
sceneElements.sceneGraph = new THREE.Scene();
// ************************** //
// Add camera
// ************************** //
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(45, width/height, 0.1, 500);
sceneElements.camera = camera;
camera.position.set(-5, 5, 5);
camera.lookAt(0, 0, 0);
// ************************** //
// Add an ambient light
// ************************** //
const ambientLight = new THREE.AmbientLight( 'rgb(255, 255, 255)', 0.2 );
sceneElements.sceneGraph.add(ambientLight);
// ************************** //
// Add spotlight (with shadow)
// ************************** //
const spotLight = new THREE.SpotLight('rgb(255, 255, 255)', 0.8);
spotLight.position.set(-5, 8, 0);
sceneElements.sceneGraph.add(spotLight);
// Setup shadow properties for the spotlight
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 2048;
spotLight.shadow.mapSize.height = 2048;
// ************************** //
// Create renderer (with shadow map)
// ************************** //
const renderer = new THREE.WebGLRenderer( {antialias:true} );
sceneElements.renderer = renderer;
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setClearColor('rgb(255, 255, 150)', 1.0);
renderer.setSize( width, height );
// Setup shadowMap property
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// ************************** //
// Add the render image in the HTML DOM
// ************************** //
const htmlElement = document.querySelector("#Tag3DScene");
htmlElement.appendChild(renderer.domElement);
}
// Create and insert in the scene graph the shapes of the 3D scene
function load3DObjects(sceneGraph) {
// ************************** //
// Create a ground plane
// ************************** //
const planeGeometry = new THREE.PlaneGeometry( 6, 6 );
const planeMaterial = new THREE.MeshPhongMaterial( {color:'rgb(200, 200, 200)', side:THREE.DoubleSide} );
const planeObject = new THREE.Mesh( planeGeometry, planeMaterial );
sceneGraph.add( planeObject );
// Change orientation of the plane using rotation
planeObject.rotateOnAxis(new THREE.Vector3(1,0,0), Math.PI/2 );
// Set shadow property
planeObject.receiveShadow = true;
// ************************** //
// Create a cube
// ************************** //
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshPhongMaterial( {color:'rgb(255,0,0)'} );
const cubeObject = new THREE.Mesh( cubeGeometry, cubeMaterial );
sceneGraph.add(cubeObject);
// Set position of the cube
cubeObject.translateY(0.5);
// Set shadow property
cubeObject.castShadow = true;
cubeObject.receiveShadow = true;
// ************************** //
// Create a sphere
// ************************** //
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32);
const sphereMaterial = new THREE.MeshPhongMaterial( {color:'rgb(180,180,255)'} );
const sphereObject = new THREE.Mesh( sphereGeometry, sphereMaterial );
sceneGraph.add(sphereObject);
// Set position of the sphere
sphereObject.translateX(-1.2).translateY(0.5).translateZ(-0.5);
// Set shadow property
sphereObject.castShadow = true;
// ************************** //
// Create a cylinder
// ************************** //
const cylinderGeometry = new THREE.CylinderGeometry(0.2, 0.2, 1.5, 25, 1);
const cylinderMaterial = new THREE.MeshPhongMaterial( {color:'rgb(200,255,150)'} );
const cylinderObject = new THREE.Mesh( cylinderGeometry, cylinderMaterial );
sceneGraph.add(cylinderObject);
// Set position of the cylinder
cylinderObject.translateX(0.5).translateY(0.75).translateZ(1.5);
// Set shadow property
cylinderObject.castShadow = true;
}
function render(sceneElements) {
sceneElements.renderer.render(sceneElements.sceneGraph, sceneElements.camera);
}
// ************************************* //
// Beginning of the Script
// ************************************* //
// General variable storing the scene graph, and elements usefull to render the scene (camera, renderer, etc)
const sceneElements = {
sceneGraph : null,
camera : null,
renderer : null,
};
// Three functions are called
// 1. Initialize an empty scene
// 2. Add elements within the scene
// 3. Render the scene
initEmptyScene(sceneElements);
load3DObjects(sceneElements.sceneGraph);
render(sceneElements);
Notes on code
General structure
-
In this code, we defined three functions [1]
-
To initialize an empty scene with all the basic elements (Three.js graph scene, camera, lighting, rendering, etc).
-
To create the shapes within the 3D scene.
-
To compute the rendering
-
-
After the definition of the functions, the general variables used for the 3D scene are defined, and functions are called.
In addition, this one adds the following elements with respect to the first one
-
Antialiasing for the rendering
-
Use of shadows. Shadows are not automatically handled in OpenGL (/projective) rendering and requires dedicated techniques. Three.js implement the technique of Shadow Mapping. In Three.js, you should explicitly setup
-
Shadow Mapping in the renderer
-
Make the lights casting shadows (and here setup the size of the texture map associated to the shadow)
-
Define for each object of the scene if its cast and/or receive shadow.
-
-
Create new 3D primitives (plane, sphere, cylinder) and change their orientation using rotateOnAxis(axis: Vector3, angle: Float) function [2].
Variables
Note that the variables of the scene (camera, sceneGraphe, etc) are defined after the functions, and are passed as parameters to them. It is also possible to define the variable before, and use them as global variables: in this case, they don’t have to be parameters of the functions
// Example of using sceneElements as global variables
const sceneElements = {
sceneGraph : null,
camera : null,
renderer : null,
};
// Functions don't need to receive parameters
function initEmptyScene( ) { ... }
function load3DObjects( ) { ... }
function render( ) { ... }
Object Three.js
All Three.js objects are instantiated using the syntax new THREE.ObjectName.
Don’t forget the operator new when instanciating a new object [5].
One of the commonly used object in Three.js is the 3D vector: THREE.Vector3, containing (x,y,z) coordinates.
Reference, copy and clone
Take care than all JavaScript objects are handled by reference. Affecting one object to two variables, leads to variables referring to the same object.
Declaring b=a doesn’t create a new object as a copy, but only assign to the same object.
Example with Vector3 object:
const a = new THREE.Vector3(0, 1, 0);
const b = a; // b refers to the same object than a
b.x = 5;
console.log(a); // Display (5, 1, 0)
To copy an object into another one, Three.js defines the method .copy(v: Vector3)
const a = new THREE.Vector3(1, 2, 3);
const b = new THREE.Vector3(0, 0, 0);
b.copy(a); // the vector b receives a copy of the (x,y,z) values from vector a
console.log(b); // Display (1, 2, 3)
b.y = 8;
console.log(a); // Display (1, 2, 3) - modifying b doesn't modify a
console.log(b); // Display (1, 8, 3)
A shortcut to create a new object, and then copy the values of an existing object can be done by the Three.js method .clone()
const a = new THREE.Vector3(1, 2, 3);
const b = a.clone(); // equivalent to b = new THREE.Vector3(); b.copy(a);
b.y = 8;
console.log(a); // Display (1, 2, 3) - modifying b doesn't modify a
console.log(b); // Display (1, 8, 3)
As a side note, pay attention that some Three.js functions receive Vector3 as parameters, while others expect 3 (x,y,z) float coordinates. Check carefully the expected parameters: JavaScript automatically adapt the type of the variables, thus sending a Vector3 object into a function expecting three (x,y,z) values will assign x to the Vector3, and undefined to y and z (leads to unexpected result and hard to debug code).
DoubleSide rendering
Three.js renders by default only one side of 3D shape (conventionally called positive side oriented in the direction of the normal).
Thus in our case, the plane could be by default only from one side and not from the other. Take care that this is a common mistake explaining why some object may not appear on the screen.
In order to be able to see shapes independently of their orientation, you can add the request to display both side with the syntax side:THREE.DoubleSide in the material associated with the object.
Use of external file
It may be interesting to split the code into multiple files. This can help readability of each file, as well as reusability.
Extra notes
-
helper.jsis included in the header of the HTML file. This inclusion should be placed after the inclusion of Three.js, and beforescene.js. -
In this example,
helper.jscontains a dictionary where elements are functions. This principle enables the use of the syntaxhelper.functionName(…)when called, which imitates the principle of library namespace (avoids function name conflicts from different libraries). [6] -
In the file
scene.jsthe functionload3DObjectsis called before its definition. This is possible in JavaScript, all functions from the same file are parsed by the engine before executing the code.
// call of the function
functionName(parameters...);
...
// definition of the function
function functionName(parameters...) {
... definition of the function ...
}
Exercise
Modify your code (either in the one or multiple file) to obtain the following result
[Website]
Hints
-
The cylinders have only been rotated with respect to their initial position.
-
Don’t forget that parameters about cylinder constructor are documented on the Three.js website.
-
Don’t forget that only one side of the object is displayed by default.