/**
* @file
*
* Summary.
* <p>Renders a Christmas scene - Merry (Early) Christmas.</p>
*
* @author Paulo Roma
* @author Flavia Cavalcanti
* @copyright © 2022-2024 Paulo R Cavalcanti.
* @since 10/02/2024
*
* @license Licensed under the {@link https://www.opensource.org/licenses/mit-license.php MIT license}.
*
* @see <a href="/cwdc/13-webgl/homework/Christmas_tree_with_three.js_new_files/script.js">source</a>
* @see <a href="/cwdc/13-webgl/homework/Christmas_tree_with_three.js_new.html">link</a>
* @see <a href="/cwdc/13-webgl/homework/img">images</a>
* @see <a href="/cwdc/13-webgl/homework/textures/cube">cube textures</a>
* @see <a href="../../img/tree.png"><img src="../../img/tree.png" width="512"></a>
* @see <a href="../../img/tree.shadow.png"><img src="../../img/tree.shadow.png" width="512"></a>
*/
"use strict";
import * as THREE from "three";
import { VertexNormalsHelper } from "three/addons/helpers/VertexNormalsHelper.js";
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
/**
* Three.js module.
* @external three
* @author Ricardo Cabello ({@link https://coopermrdoob.weebly.com/ Mr.doob})
* @since 24/04/2010
* @license Licensed under the {@link https://www.opensource.org/licenses/mit-license.php MIT license}
* @see {@link https://threejs.org/docs/#manual/en/introduction/Installation Installation}
* @see {@link https://discoverthreejs.com DISCOVER three.js}
* @see {@link https://riptutorial.com/ebook/three-js Learning three.js}
* @see {@link https://github.com/mrdoob/three.js github}
* @see {@link http://cindyhwang.github.io/interactive-design/Mrdoob/index.html An interview with Mr.doob}
* @see {@link https://experiments.withgoogle.com/search?q=Mr.doob Experiments with Google}
*/
/**
* <p>Main three.js namespace.</p>
* Imported from {@link THREE three.module.js}
*
* @example
* <!-- The recommended way of importing three.js is by using an importmap in the HTML file -->
* <script type="importmap">
* {
* "imports": {
* "three": "https://cdn.jsdelivr.net/npm/three@latest/build/three.module.js",
* "three/addons/": "https://cdn.jsdelivr.net/npm/three@latest/examples/jsm/"
* }
* }
* </script>
*
* @example
* // Then, in the javascript file:
* import * as THREE from "three";
* import { OrbitControls } from "three/addons/controls/OrbitControls.js";
*
* @example
* // Or, if you do not want an importmap:
* import * as THREE from "https://unpkg.com/three@latest/build/three.module.js?module";
* import { OrbitControls } from "https://unpkg.com/three@latest/examples/jsm/controls/OrbitControls.js?module";
*
* @namespace THREE
*/
/**
* <p>loader for loading a .obj resource.</p>
* The OBJ file format is a simple data-format that represents 3D geometry in a human readable format
* as the position of each vertex, the UV position of each texture coordinate vertex, vertex normals,
* and the faces that make each polygon defined as a list of vertices, and texture vertices.
*
* @class OBJLoader
* @memberof THREE
* @see {@link https://threejs.org/docs/#examples/en/loaders/OBJLoader OBJLoader}
*/
/**
* This is the base class for most objects in three.js and
* provides a set of properties and methods for manipulating objects in 3D space.
*
* @class Object3D
* @memberof THREE
* @see {@link https://threejs.org/docs/#api/en/core/Object3D Object3D}
*/
/**
* <p>This is almost identical to an Object3D.</p>
* Its purpose is to make working with groups of objects syntactically clearer.
*
* @class Group
* @memberof THREE
* @see {@link https://threejs.org/docs/#api/en/objects/Group Group}
*/
/**
* Scenes allow you to set up what and where is to be rendered by three.js.
* This is where you place objects, lights and cameras.
*
* @class Scene
* @memberof THREE
* @see {@link https://threejs.org/docs/#api/en/scenes/Scene Scene}
*/
/**
* <p>Camera that uses perspective projection.</p>
*
* This projection mode is designed to mimic the way the human eye sees.
* It is the most common projection mode used for rendering a 3D scene.
*
* @class PerspectiveCamera
* @memberof THREE
* @see {@link https://threejs.org/docs/#api/en/cameras/PerspectiveCamera PerspectiveCamera}
*/
/**
* <p>Class representing a color.</p>
*
* Iterating through a Color instance will yield its components (r, g, b)
* in the corresponding order.
*
* @class Color
* @memberof THREE
* @see {@link https://threejs.org/docs/#api/en/math/Color Color}
*/
/**
* Abstract base class for materials.
*
* Materials describe the appearance of objects.
* They are defined in a (mostly) renderer-independent way,
* so you don't have to rewrite materials if you decide to use a different renderer.
*
* @class Material
* @memberof THREE
* @see {@link https://threejs.org/docs/#api/en/materials/Material Material}
* @see {@link https://threejs.org/docs/#api/en/materials/MeshBasicMaterial MeshBasicMaterial}
* @see {@link https://threejs.org/docs/#api/en/materials/MeshLambertMaterial MeshLambertMaterial}
* @see {@link https://threejs.org/docs/#api/en/materials/MeshPhongMaterial MeshPhongMaterial}
* @see {@link https://threejs.org/docs/#api/en/materials/MeshStandardMaterial MeshStandardMaterial}
* @see {@link https://threejs.org/docs/#api/en/materials/PointsMaterial PointsMaterial}
*/
/**
* The WebGL renderer displays your beautifully crafted scenes using WebGL.
*
* @class WebGLRenderer
* @memberof THREE
* @see {@link https://threejs.org/docs/#api/en/renderers/WebGLRenderer WebGLRenderer}
*/
/**
* Three.js group.
* @type {THREE.Group}
*/
let group;
/**
* Three.js group.
* @type {THREE.Group}
*/
let teaPotGroup;
/**
* Three.js scene.
* @type {THREE.Scene}
*/
let scene;
/**
* Three.js camera.
* @type {THREE.PerspectiveCamera}
*/
let camera;
/**
* Three.js renderer.
* @type {THREE.WebGLRenderer}
*/
let renderer;
/**
* A container holding the greeting and the canvas.
* @type {HTMLDivElement}
*/
let container;
/**
* Rotation about the "Y" axis, applied by the renderer to the scene,
* based on mouse displacement.
* @type {Number}
*/
let targetRotation = 0;
/**
* Target rotation when the mouse is clicked.
* @type {Number}
*/
let targetRotationOnMouseDown = 0;
/**
* An event (clientX - {@link windowHalfX}).
* @type {Number}
*/
let mouseX = 0;
/**
* An event (clientX - {@link windowHalfX}) when a movement starts.
* @type {Number}
*/
let mouseXOnMouseDown = 0;
/**
* Half of the window {@link https://developer.mozilla.org/en-US/docs/Web/API/window/innerWidth innerWidth} property.
* @type {Number}
*/
let windowHalfX = window.innerWidth / 2;
/**
* Half of the window {@link https://developer.mozilla.org/en-US/docs/Web/API/window/innerHeight innerHeight} property.
* @type {Number}
*/
let windowHalfY = window.innerHeight / 2;
/**
* Will pause the camera rotation.
* @type {Boolean}
*/
let paused = false;
/**
* Camera will move around the tree in and out.
* @type {Boolean}
*/
let inAndOutCamera = true;
/**
* Display help.
* @type {Boolean}
*/
let help = false;
/**
* Image directory.
* @type {String}
*/
const path = "img/";
/**
* Light helpers switch.
* @type {Boolean}
*/
let showHelpers = false;
/**
* Color table.
* @type {Object<String,THREE.Color>}
*/
const colorTable = {
white: new THREE.Color(0xffffff),
red: new THREE.Color(0xff0000),
green: new THREE.Color(0x008800),
blue: new THREE.Color(0x0000ff),
lightBlue: new THREE.Color(0x00ccff),
veryLightBlue: new THREE.Color(0xd2ddef),
black: new THREE.Color(0x222222),
black2: new THREE.Color(0x111111),
blackSRGB: new THREE.Color(0x333333),
orange: new THREE.Color(0xffcc00),
yellow: new THREE.Color(0xffff00),
brown: new THREE.Color(0x995500),
lightBrown: new THREE.Color(0xcc6600),
darkBrown: new THREE.Color(0x584000),
white2: new THREE.Color(0xfffff6),
amber: new THREE.Color(0xffae00),
purple: new THREE.Color(0x590fa3),
};
/**
* Light helpers.
* @property {Object} lightHelpers - container for helpers.
* @property {THREE.DirectionalLightHelper} lightHelpers.dhelper -
* {@link https://threejs.org/docs/#api/en/helpers/DirectionalLightHelper directional light} helper.
* @property {THREE.SpotLightHelper} lightHelpers.shelper -
* {@link https://threejs.org/docs/#api/en/helpers/SpotLightHelper spot light} helper.
* @property {THREE.PointlLightHelper} lightHelpers.phelper -
* {@link https://threejs.org/docs/#api/en/helpers/PointLightHelper point light} helper.
* @property {THREE.CameraHelper} lightHelpers.chelper -
* {@link https://threejs.org/docs/#api/en/helpers/CameraHelper camera} helper.
*/
const lightHelpers = {
dhelper: null,
shelper: null,
phelper: null,
chelper: null,
};
/**
* Not the best for a skybox, but the effect is quite psychadelic.
* @type {Object<Symbol, Array<String>>}
*/
const imageNames = {
img1: [
"wrappingPaper.jpg",
"wrappingPaper.jpg",
"wrappingPaper.jpg",
"wrappingPaper.jpg",
"wrappingPaper.jpg",
"wrappingPaper.jpg",
],
img2: [
"wrappingPaper2.jpg",
"wrappingPaper2.jpg",
"wrappingPaper2.jpg",
"wrappingPaper2.jpg",
"wrappingPaper2.jpg",
"wrappingPaper2.jpg",
],
img3: ["px.png", "nx.png", "py.png", "ny.png", "pz.png", "nz.png"],
};
/**
* <p>Resizes the scene according to the screen size.</p>
*
* {@link http://benchung.com/smooth-mouse-rotation-three-js/ Many thanks}.
*/
function onWindowResize() {
windowHalfX = window.innerWidth / 2;
windowHalfY = window.innerHeight / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth - 20, window.innerHeight - 20);
}
/**
* The mousedown event is fired at an Element when a pointing device button
* is pressed while the pointer is inside the element.
*
* <p>Add listeners for "mousemove", "mouseup", and "mouseout".
* @param {MouseEvent} event mouse event.
* @event
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/mousedown_event Element: mousedown event}
*/
function onDocumentMouseDown(event) {
event.preventDefault();
renderer.domElement.addEventListener("mousemove", onDocumentMouseMove, false);
renderer.domElement.addEventListener("mouseup", onDocumentMouseUp, false);
renderer.domElement.addEventListener("mouseout", onDocumentMouseOut, false);
mouseXOnMouseDown = event.clientX - windowHalfX;
targetRotationOnMouseDown = targetRotation;
}
/**
* The mousemove event is fired at an element when a pointing device
* (usually a mouse) is moved while the cursor's hotspot is inside it.
* @param {MouseEvent} event mouse event.
* @event
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/mousemove_event Element: mousemove event}
*/
function onDocumentMouseMove(event) {
mouseX = event.clientX - windowHalfX;
targetRotation =
targetRotationOnMouseDown + (mouseX - mouseXOnMouseDown) * 0.02;
}
/**
* The mouseup event is fired at an Element when a button on a pointing device
* (such as a mouse or trackpad) is released while the pointer is located inside it.
*
* <p>Remove listeners for "mousemove", "mouseup", and "mouseout".
* @param {MouseEvent} event mouse event.
* @event
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseup_event Element: mouseup event}
*/
function onDocumentMouseUp(event) {
renderer.domElement.removeEventListener(
"mousemove",
onDocumentMouseMove,
false,
);
renderer.domElement.removeEventListener("mouseup", onDocumentMouseUp, false);
renderer.domElement.removeEventListener(
"mouseout",
onDocumentMouseOut,
false,
);
}
/**
* The mouseout event is fired at an Element when a
* pointing device (usually a mouse) is used to move the cursor
* so that it is no longer contained within the element or one of its children.
*
* <p>Removes the listeners for "mousemove", "mouseup", and "mouseout".
* @param {MouseEvent} event mouse event.
* @event
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseout_event Element: mouseout event}
*/
function onDocumentMouseOut(event) {
renderer.domElement.removeEventListener(
"mousemove",
onDocumentMouseMove,
false,
);
renderer.domElement.removeEventListener("mouseup", onDocumentMouseUp, false);
renderer.domElement.removeEventListener(
"mouseout",
onDocumentMouseOut,
false,
);
}
/**
* The touchstart event is fired when one or more touch points
* are placed on the touch surface.
* @param {TouchEvent} event touch event.
* @event
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/touchstart_event Element: touchstart event}
*/
function onDocumentTouchStart(event) {
if (event.touches.length == 1) {
event.preventDefault();
mouseXOnMouseDown = event.touches[0].pageX - windowHalfX;
targetRotationOnMouseDown = targetRotation;
}
}
/**
* The touchmove event is fired when one or more touch points
* are moved along the touch surface.
* @param {TouchEvent} event touch event.
* @event
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/touchmove_event Element: touchmove event}
*/
function onDocumentTouchMove(event) {
if (event.touches.length == 1) {
event.preventDefault();
mouseX = event.touches[0].pageX - windowHalfX;
targetRotation =
targetRotationOnMouseDown + (mouseX - mouseXOnMouseDown) * 0.02;
}
}
/**
* Set the {@link camera} and add it to the given scene.
*
* @param {THREE.Scene} scene given scene.
*/
function makeCamera(scene) {
camera = new THREE.PerspectiveCamera(
50,
window.innerWidth / window.innerHeight,
1,
10000,
);
camera.position.set(0, 100, 500);
scene.add(camera);
}
/**
* Translate keydown events to strings.
*
* @param {KeyboardEvent} event keyboard event.
* @see {@link https://javascript.info/tutorial/keyboard-events Keyboard: keydown and keyup}
*/
function getChar(event) {
event = event || window.event;
const charCode = event.key || String.fromCharCode(event.which);
return charCode;
}
/**
* The camera control.
* @param {THREE.Camera} c the given camera.
* @param {String} ch a given character.
*/
function cameraControl(c, ch) {
const distance = c.position.length();
let q, q2;
switch (ch) {
// camera controls
case "w":
c.translateZ(-3);
return true;
case "a":
c.translateX(-3);
return true;
case "s":
c.translateZ(3);
return true;
case "d":
c.translateX(3);
return true;
case "ArrowUp":
c.translateY(3);
return true;
case "ArrowDown":
c.translateY(-3);
return true;
case "j":
// need to do extrinsic rotation about world y axis, so multiply camera's quaternion
// on left
q = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
(5 * Math.PI) / 180,
);
q2 = new THREE.Quaternion().copy(c.quaternion);
c.quaternion.copy(q).multiply(q2);
return true;
case "l":
q = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
(-5 * Math.PI) / 180,
);
q2 = new THREE.Quaternion().copy(c.quaternion);
c.quaternion.copy(q).multiply(q2);
return true;
case "i":
// intrinsic rotation about camera's x-axis
c.rotateX((5 * Math.PI) / 180);
return true;
case "k":
c.rotateX((-5 * Math.PI) / 180);
return true;
case "O":
c.lookAt(new THREE.Vector3(0, 0, 0));
return true;
case "-":
c.fov = Math.min(80, c.fov + 5);
c.updateProjectionMatrix();
return true;
case "+":
c.fov = Math.max(5, c.fov - 5);
c.updateProjectionMatrix();
return true;
// alternates for arrow keys
case "J":
//this.orbitLeft(5, distance)
c.translateZ(-distance);
q = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
(5 * Math.PI) / 180,
);
q2 = new THREE.Quaternion().copy(c.quaternion);
c.quaternion.copy(q).multiply(q2);
c.translateZ(distance);
return true;
case "L":
//this.orbitRight(5, distance)
c.translateZ(-distance);
q = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
(-5 * Math.PI) / 180,
);
q2 = new THREE.Quaternion().copy(c.quaternion);
c.quaternion.copy(q).multiply(q2);
c.translateZ(distance);
return true;
case "I":
//this.orbitUp(5, distance)
c.translateZ(-distance);
c.rotateX((-5 * Math.PI) / 180);
c.translateZ(distance);
return true;
case "K":
//this.orbitDown(5, distance)
c.translateZ(-distance);
c.rotateX((5 * Math.PI) / 180);
c.translateZ(distance);
return true;
}
return false;
}
/**
* Handler for key press events.
* @param {KeyboardEvent} event keydown event.
*/
function handleKeyPress(event) {
const ch = getChar(event);
cameraControl(camera, ch);
switch (ch) {
case " ":
paused = !paused;
break;
case "n":
inAndOutCamera = !inAndOutCamera;
break;
case "h":
help = !help;
if (help) {
document.getElementById("info").innerHTML = `DRAG TO SPIN <br><br>
<b>Keyboard controls</b>:<br>
<b>h - to hide</b><br>
<b>l</b> - toggle light helpers<br>
<b>w, s, a, d</b> - move forward, backward, left, right <br>
<b>↑, ↓</b> - move up, down <br>
<b>I, K, J, L</b> - orbit down, up, right, left <br>
<b>+</b> - decrease fov <br>
<b>-</b> - increase fov <br>
<b>Space</b> - pause animation <br>
<b>n</b> - camera will rotate around the tree,<br>
while moving closer/farther away, or not.`;
} else
document.getElementById("info").innerHTML = `DRAG TO SPIN<br>
Have your volume ON for the full experience <br>
Press <b>h</b> for more information.`;
break;
case "l":
displayHelpers();
break;
default:
return;
}
}
/**
* Prepare materials and creates the {@link makeTree tree}.
* @param {THREE.Group} group - the given group.
*/
function prepareMaterials(group) {
// prettier-ignore
const imgs = [ // materials []
"pine.jpg", // 0, 1, [2], 3
"wood.jpg", // [4]
"red.jpg", // [5]
"blue.jpg", // [6]
"green.jpg", // [7]
"yellow.jpg", // [8]
"moon.jpg", // 9
];
const shininess = 50;
const specular = colorTable.blackSRGB;
const bumpScale = 1;
const shading = false;
const mats = {};
const tLoader = new THREE.TextureLoader();
imgs.forEach((img, index) => {
const texture = tLoader.load(path + img);
texture.repeat.set(1, 1);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.anisotropy = 16;
texture.colorSpace = THREE.SRGBColorSpace;
const key = img.split(".")[0];
if (index == 0) {
mats[`${key}0`] = new THREE.MeshPhongMaterial({
map: texture,
bumpMap: texture,
bumpScale: bumpScale,
color: colorTable.red,
flatShading: shading,
specular: specular,
shininess: shininess,
});
mats[`${key}1`] = new THREE.MeshPhongMaterial({
map: texture,
color: colorTable.green,
flatShading: shading,
specular: specular,
shininess: shininess,
});
mats[`${key}3`] = new THREE.MeshPhongMaterial({
map: texture,
color: colorTable.red,
flatShading: shading,
});
}
// this is what is really used
mats[key] = new THREE.MeshPhongMaterial({
map: texture,
color: colorTable.darkBrown,
flatShading: shading,
});
});
makeTree(group, mats);
}
/**
* <p>I developed a certain dislike for skyboxes or at least for the clunky ones.
* As such, the PRESENTS are going to be skyboxes.</p>
*
* Why not, am I right? No specification were given saying that the
* skyboxes had to be used as the 'environment'.
* @param {THREE.Group} group - the given group to add the presents to.
* @param {Number} size - the size of the box -- will correspond to width, length, and height.
* @param {Number} x - position x
* @param {Number} y - position y
* @param {Number} z - position z
* @param {Array<String>} images - image array to use.
* @param {String} imgpath - path to the image array.
* @see {@link https://threejs.org/examples/?q=cube#webgpu_cubemap_adjustments Env. Adjustments example}
* @see {@link https://threejs.org/docs/#api/en/materials/MeshStandardMaterial MeshStandardMaterial}
*/
function addPresent(group, size, x, y, z, images, imgpath = path) {
// load the six images
const textureMap = new THREE.CubeTextureLoader()
.setPath(imgpath)
.load(images);
textureMap.colorSpace = THREE.SRGBColorSpace;
textureMap.generateMipmaps = true;
textureMap.minFilter = THREE.LinearMipmapLinearFilter;
textureMap.magFilter = THREE.LinearFilter;
const boxMaterial = new THREE.MeshLambertMaterial({
envMap: textureMap,
color: colorTable.white,
side: THREE.FrontSide,
reflectivity: 1,
combine: THREE.MixOperation,
});
let cube;
if (imgpath != path) {
//scene.environment = textureMap;
//scene.background = textureMap;
const sphereMaterial = new THREE.MeshStandardMaterial({
roughness: 0,
metalness: 1,
envMap: textureMap,
});
cube = new THREE.Mesh(
new THREE.SphereGeometry(size, 32, 16),
sphereMaterial,
);
} else {
// Create a mesh for the object, using the cube shader as the material
cube = new THREE.Mesh(new THREE.BoxGeometry(size, size, size), boxMaterial);
}
cube.position.set(x, y, z);
cube.castShadow = true;
cube.name = "Present";
// add it to the scene
group.add(cube);
if (typeof addPresent.counter == "undefined") {
addPresent.counter = 0;
}
lightHelpers[`p${++addPresent.counter}helper`] = new VertexNormalsHelper(
cube,
5,
colorTable.white,
);
lightHelpers[`p${addPresent.counter}helper`].name = "Present";
}
/**
* <p>Add ground to scene.</p>
*
* The average user doesn't have a calibrated monitor and has never heard of gamma correction;
* therefore, many visual materials are precorrected for them.
* For example, by convention, all JPEG files are precorrected for a gamma of 2.2.
* That's not exact for any monitor, but it's in the ballpark, so the image will probably
* look acceptable on most monitors. This means that JPEG images
* (including scans and photos taken with a digital camera) are not linear,
* so they should not be used as texture maps by shaders that assume linear input.
*
* @param {THREE.Group} group - the given group to add the ground to.
* @see {@link https://developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-24-importance-being-linear The Importance of Being Linear}
*/
function addGround(group) {
const groundColor = colorTable.veryLightBlue;
const groundTexture2 = new THREE.DataTexture(groundColor, 1, 1);
const groundMaterial = new THREE.MeshPhongMaterial({
color: colorTable.white,
specular: colorTable.black2,
map: groundTexture2,
side: THREE.DoubleSide,
});
const groundTexture = new THREE.TextureLoader().load(
path + "ground.jpg",
function () {
groundMaterial.map = groundTexture;
},
);
groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
groundTexture.repeat.set(25, 25);
groundTexture.anisotropy = 16;
groundTexture.colorSpace = THREE.SRGBColorSpace;
const groundMesh = new THREE.Mesh(
new THREE.PlaneGeometry(20000, 20000),
groundMaterial,
);
groundMesh.position.y = -150;
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.receiveShadow = true;
groundMesh.name = "Ground";
group.add(groundMesh);
}
/**
* <p>Christmas needs frigging snowflakes.</p>
* Except Christmas in Brazil, then it is just palm trees...
* Based on a tutorial found on {@link https://script-tutorials.com/tag/webgl/ huzzah}
* @param {THREE.Group} group - the given group to add the snowflakes to.
*/
function addSnowflakes(group) {
const sfGeometry = new THREE.BufferGeometry();
const sfMats = [];
const tLoader = new THREE.TextureLoader();
const sfTexture = tLoader.load(path + "snowflake.png");
const sfTexture2 = tLoader.load(path + "snowflake2.png");
sfTexture.colorSpace = THREE.SRGBColorSpace;
sfTexture2.colorSpace = THREE.SRGBColorSpace;
const vertices = [];
for (let i = 0; i < 3700; i++) {
const vertex = new THREE.Vector3();
vertex.x = Math.random() * 2000 - 1000;
vertex.y = Math.random() * 2000 - 1000;
vertex.z = Math.random() * 2000 - 1000;
vertices.push(vertex.x, vertex.y, vertex.z);
}
sfGeometry.setAttribute(
"position",
new THREE.Float32BufferAttribute(vertices, 3),
);
const states = [
{ color: [1.0, 0.2, 0.9], sprite: sfTexture, size: 10 },
{ color: [0.9, 0.1, 0.5], sprite: sfTexture, size: 8 },
{ color: [0.8, 0.05, 0.5], sprite: sfTexture, size: 5 },
{ color: [1.0, 0.2, 0.9], sprite: sfTexture2, size: 10 },
{ color: [0.9, 0.1, 0.5], sprite: sfTexture2, size: 8 },
{ color: [0.8, 0.05, 0.5], sprite: sfTexture2, size: 5 },
];
states.forEach((state) => {
const i = sfMats.push(
new THREE.PointsMaterial({
size: state.size,
map: state.sprite,
blending: THREE.AdditiveBlending,
depthTest: false,
transparent: true,
}),
);
sfMats[i - 1] = new THREE.PointsMaterial({
size: state.size,
map: state.sprite,
blending: THREE.AdditiveBlending,
depthTest: false,
transparent: true,
});
sfMats[i - 1].color.setHSL(...state.color);
const particles = new THREE.Points(sfGeometry, sfMats[i - 1]);
particles.rotation.x = Math.random() * 15;
particles.rotation.y = Math.random() * 10;
particles.rotation.z = Math.random() * 17;
particles.name = "Particles";
group.add(particles);
});
}
/**
* Make the Christmas tree, which is just a bunch of stacked cones (cylinders).
* @param {THREE.Object3D} group - the given group to add the tree to.
* @param {Object<String,THREE.Material>} materials - the given material object.
* @see {@link https://threejs.org/docs/#api/en/geometries/CylinderGeometry CylinderGeometry}
*/
function makeTree(group, materials) {
// radius top, radius bottom, height, radial segments, height segments.
const tree = [
{ geometry: [1, 30, 50, 30, 1], y: 130, material: materials.pine },
{ geometry: [1, 40, 70, 30, 1], y: 110, material: materials.pine },
{ geometry: [1, 50, 80, 30, 1], y: 85, material: materials.pine },
{ geometry: [1, 60, 90, 30, 1], y: 65, material: materials.pine },
{ geometry: [1, 70, 80, 30, 1], y: 30, material: materials.pine },
{ geometry: [1, 80, 90, 30, 1], y: 5, material: materials.pine },
{ geometry: [1, 95, 95, 30, 1], y: -20, material: materials.pine },
{ geometry: [2, 20, 300, 30, 1], y: 0, material: materials.wood },
];
if (typeof makeTree.counter == "undefined") {
makeTree.counter = 0;
}
tree.forEach((elem) => {
const t = new THREE.Mesh(
new THREE.CylinderGeometry(...elem.geometry, false),
elem.material,
);
t.castShadow = true;
t.position.set(0, elem.y, 0);
t.name = "Tree";
group.add(t);
lightHelpers[`t${++makeTree.counter}helper`] = new VertexNormalsHelper(
t,
5,
colorTable.white,
);
lightHelpers[`t${makeTree.counter}helper`].name = "Tree";
});
addBaubles(group, materials);
}
/**
* <p>Add 28 baubles.</p>
* <p>Yeah, kind of hardcoded... no, I'm not proud.</p>
* But this was the most straightforward way to add trinkets
* to the tree that actually looked like they were on the tree.
* @param {THREE.Group} group - the given group to add the baubles to.
* @param {Object<String,THREE.Material>} materials - the given material object.
*/
function addBaubles(group, materials) {
const bauble = [
{ geometry: [5, 15, 5], position: [15, 135, 5], color: materials.red },
{ geometry: [5, 15, 5], position: [0, 135, 13], color: materials.yellow },
{ geometry: [5, 15, 5], position: [0, 135, -13], color: materials.red },
{ geometry: [5, 15, 5], position: [-15, 135, -5], color: materials.yellow },
{ geometry: [6, 15, 5], position: [35, 90, 5], color: materials.blue },
{ geometry: [5, 15, 5], position: [0, 90, 33], color: materials.red },
{ geometry: [6, 15, 5], position: [-35, 90, -5], color: materials.blue },
{ geometry: [5, 15, 5], position: [0, 90, -33], color: materials.red },
{ geometry: [7, 15, 5], position: [35, 60, 25], color: materials.green },
{ geometry: [5, 15, 5], position: [-30, 60, 33], color: materials.yellow },
{ geometry: [7, 15, 5], position: [-35, 60, -25], color: materials.green },
{ geometry: [5, 15, 5], position: [30, 60, -33], color: materials.yellow },
{ geometry: [8, 15, 5], position: [48, 35, 25], color: materials.red },
{ geometry: [5, 15, 5], position: [-42, 35, 33], color: materials.blue },
{ geometry: [8, 15, 5], position: [-48, 35, -25], color: materials.red },
{ geometry: [5, 15, 5], position: [42, 35, -33], color: materials.blue },
{ geometry: [6, 15, 5], position: [-52, 7, 25], color: materials.yellow },
{ geometry: [5, 15, 5], position: [50, 7, 33], color: materials.green },
{ geometry: [6, 15, 5], position: [52, 7, -25], color: materials.yellow },
{ geometry: [5, 15, 5], position: [-50, 7, -33], color: materials.green },
{ geometry: [7, 15, 5], position: [65, -25, 25], color: materials.blue },
{ geometry: [5, 15, 5], position: [-30, -25, 63], color: materials.red },
{ geometry: [7, 15, 5], position: [-65, -25, -25], color: materials.blue },
{ geometry: [5, 15, 5], position: [30, -25, -63], color: materials.red },
{ geometry: [8, 15, 5], position: [80, -50, 25], color: materials.red },
{ geometry: [6, 15, 5], position: [-40, -50, 73], color: materials.yellow },
{ geometry: [8, 15, 5], position: [-80, -50, -25], color: materials.red },
{ geometry: [6, 15, 5], position: [40, -50, -73], color: materials.yellow },
];
bauble.forEach((e, i) => {
const b = new THREE.Mesh(new THREE.SphereGeometry(...e.geometry), e.color);
b.position.set(...e.position);
b.name = `bauble${i}`;
group.add(b);
});
}
/**
* <p>Loads an object to the scene.</p>
* Used to add the teapot and the bunnies.
* Frigging bunnies all around, everyone loves bunnies.
* Teapot is our new Christmas tree star.
*
* @param {THREE.Group} group - the given group to add the object to.
* @param {String} objectFile - the object file to be read.
* @param {Number} x - position x
* @param {Number} y - position y
* @param {Number} z - position z
* @param {Number} size - the object's size.
* @param {Number} rotate - rotation amount.
* @param {THREE.Color} color - the objects's color.
*/
function addObject(group, objectFile, x, y, z, size, rotate, color) {
if (typeof addObject.counter == "undefined") {
addObject.counter = 0;
}
/**
* ObjectLoader object.
* @var {THREE.OBJLoader}
* @global
*/
const oLoader = new OBJLoader();
oLoader.load(objectFile, function (object) {
const material2 = new THREE.MeshLambertMaterial({ color: color });
object.position.set(x, y, z);
object.scale.set(size, size, size);
object.rotateY(rotate);
object.traverse(function (child) {
if (child instanceof THREE.Mesh) {
// apply custom material
child.material = material2;
// enable casting shadows
child.castShadow = true;
child.receiveShadow = true;
const obj = `o${++addObject.counter}helper`;
lightHelpers[obj] = new VertexNormalsHelper(child, 5, colorTable.white);
lightHelpers[obj].update();
lightHelpers[obj].name = objectFile;
lightHelpers[obj].visible = false;
group.add(lightHelpers[obj]);
}
});
object.name = objectFile;
group.add(object);
});
}
/**
* Add or remove light or normal {@link showHelpers helpers}.
*/
function displayHelpers() {
showHelpers = !showHelpers;
// because of strict mode, "this" is undefined
const action = showHelpers ? group.add.bind(group) : group.remove.bind(group);
const action2 = showHelpers
? scene.add.bind(scene)
: scene.remove.bind(scene);
Object.keys(lightHelpers).forEach((key) => {
if (["teapot.obj", "bunny.obj"].includes(lightHelpers[key].name)) {
// not working - only teapot untransformed??!!
//lightHelpers[key].visible = showHelpers;
} else if (
["PointLight", "SpotLight", "DirectionalLight", "CameraHelper"].includes(
lightHelpers[key].name,
)
) {
action2(lightHelpers[key]);
} else {
action(lightHelpers[key]);
}
});
return false;
}
/**
* <p>Lights galore - includes {@link https://threejs.org/docs/#api/en/lights/PointLight point lights},
* {@link https://threejs.org/docs/#api/en/lights/SpotLight spot lights},
* and a {@link https://threejs.org/docs/#api/en/lights/DirectionalLight directional light}
* because why not?</p>
*
* <p>We also created a
* {@link https://threejs.org/docs/#api/en/helpers/CameraHelper camera helper}
* for the spot light.</p>
*
* <p>Lighting and color has changed a lot since version
* {@link https://discourse.threejs.org/t/updates-to-lighting-in-three-js-r155/53733/23 155}.</p>
*
* It’s important to understand that using the new lighting mode is just one prerequisite for physically correct lighting.<br>
* You also have to:
* <ul>
* <li> apply a real-world scale to your scene (meaning 1 world unit = 1 meter).</li>
* <li>not change the default decay values of 2 for all spot and point lights in your scene.</li>
* </ul>
* <p>Only then you can actually consider SI units for point, spot and area lights.
* Ambient and hemisphere lights (which are special kind of lights and
* essentially simplified models of light probes), as well as directional lights, do not use SI units.</p>
*
* <img src="../../img/unit-of-light.jpg" width="256">
*
* <p>These updates enable a
* {@link https://www.willgibbons.com/linear-workflow/ “linear workflow”}
* by default, for better
* {@link https://developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-24-importance-being-linear image quality}.</p>
*
* <p>To set “renderer.useLegacyLights = false;” in {@link init},
* I had to increase the light intensities, <br>
* and set decay to zero (my lights are too far away from the scene borders):</p>
* <ul>
* <li>ambient light 0.2 → 2
* <li>point light 2 → 4</li>
* <li>spot light 1 → 2</li>
* <li>ambient light 2 → Math.PI * 2</li>
* <li>pointLight.decay = 0;</li>
* <li>spotLight.decay = 0;</li>
* </ul>
*
* @param {THREE.Scene} scene - the given scene.
* @see {@link https://discourse.threejs.org/t/shadow-and-color-problems-going-from-v64-to-v161/61640/3 Shadow and color problems going from v64 to v161}
*/
function addLight(scene) {
scene.add(new THREE.AmbientLight(colorTable.black, 2));
const pointLight = new THREE.PointLight(colorTable.lightBlue, 4, 1000);
pointLight.position.set(200, 100, 0);
pointLight.castShadow = false;
pointLight.decay = 0;
scene.add(pointLight);
lightHelpers.phelper = new THREE.PointLightHelper(pointLight, 10);
lightHelpers.phelper.name = "PointLight";
const spotLight = new THREE.SpotLight(colorTable.white, 2);
spotLight.position.set(200, 200, 200);
spotLight.angle = Math.PI / 6;
spotLight.decay = 0;
spotLight.distance = 0;
spotLight.penumbra = 0.2;
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 0.5;
spotLight.shadow.camera.far = 5000;
spotLight.shadow.camera.fov = 60;
spotLight.shadow.focus = 1;
scene.add(spotLight);
lightHelpers.chelper = new THREE.CameraHelper(spotLight.shadow.camera);
lightHelpers.chelper.name = "CameraHelper";
lightHelpers.shelper = new THREE.SpotLightHelper(spotLight);
lightHelpers.shelper.name = "SpotLight";
// colored directional light at double intensity shining from the top.
const directionalLight = new THREE.DirectionalLight(
colorTable.white,
Math.PI * 2,
);
directionalLight.position.set(400, 1, 200);
directionalLight.castShadow = false;
scene.add(directionalLight);
lightHelpers.dhelper = new THREE.DirectionalLightHelper(directionalLight, 15);
lightHelpers.dhelper.name = "DirectionalLight";
}
/**
* Display a greeting and information at the top of the page.
*/
function makeGreeting() {
// prepare the container
container = document.createElement("div");
document.body.appendChild(container);
// display Info
const greeting = document.createElement("div");
greeting.setAttribute("id", "greeting");
greeting.innerHTML = "<b>MERRY CHRISTMAS!</b><br>";
const info = document.createElement("div");
info.setAttribute("id", "info");
greeting.setAttribute("id", "greeting");
greeting.style.position = "absolute";
greeting.style.top = "30px";
greeting.style.width = "100%";
greeting.style.textAlign = "center";
greeting.style.color = "white";
info.innerHTML = `<details>
<summary>DRAG TO SPIN</summary>
Have your volume ON for the full experience<br>
Press <em>h</em> for more information<br>
For light helpers
<a href='javascript:void(0)' onclick='javascript:displayHelpers();'>click me</a>
</details>`;
greeting.appendChild(info);
container.appendChild(greeting);
}
/**
* Initialize our scene's components.
*
* <p>Add listeners for {@link event:onDocumentMouseDown "mousedown"},
* {@link event:onDocumentTouchStart "touchstart"}, and
* {@link event:onDocumentTouchMove "touchmove"}.</p>
*
* The listeners are added to the canvas element
* ({@link https://threejs.org/docs/#api/en/renderers/WebGLRenderer.domElement renderer.domElement}),
* so to prevent
* {@link https://usefulangle.com/post/278/html-disable-pull-to-refresh-with-css "the pull to refresh"}
* on a swipe-down.
*/
function init() {
makeGreeting();
THREE.ColorManagement.enabled = true;
// initialize the scene
scene = new THREE.Scene();
// add fog to scene
scene.fog = new THREE.Fog(colorTable.purple, 500, 10000);
makeCamera(scene);
// create the empty scene groups
group = new THREE.Group();
group.name = "MainGroup";
teaPotGroup = new THREE.Group();
teaPotGroup.name = "TeaPot";
scene.add(group);
group.add(teaPotGroup);
prepareMaterials(group);
// add 4 skyboxes
addPresent(group, 40, 20, -115, -130, imageNames.img3, "textures/cube/pisa/");
addPresent(group, 50, 20, -125, 60, imageNames.img2);
addPresent(group, 30, -20, -135, -60, imageNames.img1);
addPresent(group, 20, -20, -140, 100, imageNames.img1);
// add our star teapot
addObject(teaPotGroup, "teapot.obj", 0, 155, 0, 0.3, 0, colorTable.yellow);
// bunnies for days
addObject(group, "bunny.obj", 80, -130, 0, 20, 0, colorTable.brown);
addObject(group, "bunny.obj", -50, -140, 0, 10, -90, colorTable.lightBrown);
addObject(group, "bunny.obj", 100, -135, 60, 15, 40, colorTable.white);
addObject(group, "bunny.obj", 20, -143, -60, 8, -180, colorTable.amber);
addGround(group);
addSnowflakes(group);
addLight(scene);
// prepare the render object and render the scene
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setClearColor(scene.fog.color);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
// add events handlers -- thanks script tutorials
renderer.domElement.addEventListener("mousedown", onDocumentMouseDown, false);
renderer.domElement.addEventListener(
"touchstart",
onDocumentTouchStart,
false,
);
renderer.domElement.addEventListener("touchmove", onDocumentTouchMove, false);
/**
* <p>Key handler.</p>
* <p>Fired when a key is pressed.</p>
* Calls {@link handleKeyPress} when pressing assigned keys:
* <ul>
* <li>Space - pause</li>
* <li>h - help</li>
* <li>l - light helpers</li>
* <li>w, s, a, d - forward, backward, left, right</li>
* <li>I, K, J, L - orbit down, up, left, right</li>
* <li>+, - - field of view (zoom)</li>
* <li>↑, ↓- up, down</li>
* <li>n - move camera close/farther away wile rotating, or not</li>
* </ul>
* @event keydown
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event Element: keydown event}
*/
document.addEventListener("keydown", handleKeyPress, false);
/**
* <p>Appends an event listener for events whose type attribute value is resize.</p>
* Fires when the document view (window) has been resized.
* <p>The {@link onWindowResize callback} argument sets the callback
* that will be invoked when the event is dispatched.</p>
* @param {Event} event the document view is resized.
* @param {callback} function function to run when the event occurs.
* @param {Boolean} useCapture handler is executed in the bubbling or capturing phase.
* @event resize
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event Window: resize event}
*/
window.addEventListener("resize", onWindowResize, false);
window.displayHelpers = displayHelpers;
/**
* <p>A built in function that can be used instead of
* {@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame requestAnimationFrame}.</p>
* The {@link https://threejs.org/docs/#api/en/renderers/WebGLRenderer.setAnimationLoop renderer.setAnimationLoop}
* parameter is a {@link render callback,} which
* will be called every available frame.<br>
* If null is passed it will stop any already ongoing animation.
* @param {function} loop callback.
* @function
* @name setAnimationLoop
* @global
*/
renderer.setAnimationLoop(() => {
render();
});
}
/**
* A closure to render the application.
* @return {animate} animation callback.
* @function
* @global
* @see {@link https://threejs.org/docs/#api/en/core/Object3D.rotation Object3D.rotation}
*/
const render = (() => {
let ang = 0;
const increment = THREE.MathUtils.degToRad(0.5);
const rotSpeed = 0.004;
/**
* Rotates the camera around the tree and
* calls the {@link renderer renderer.render} method.
* @callback animate
*/
return () => {
// mouse click and drag
group.rotation.y += (targetRotation - group.rotation.y) * 0.01;
// spinning teapot -- it is a nice star
teaPotGroup.rotation.y += 0.03;
if (!paused && inAndOutCamera) {
ang += increment;
ang %= 2 * Math.PI;
camera.position.x = Math.cos(ang) * 1000; // [-1000,1000]
camera.position.z = Math.sin(ang) * 500; // [-500,500]
}
if (!paused && !inAndOutCamera) {
// rotate camera around tree
camera.rotation.y = rotSpeed;
}
camera.lookAt(scene.position);
renderer.render(scene, camera);
};
})();
/**
* <p>Load the applicarion.</p>
* Fired when the whole page has loaded, including all dependent resources
* such as stylesheets, scripts, iframes, and images, except those that are loaded lazily.
* {@link init Initialize} and start {@link animate animation}.
* @param {Event} event a generic Event.
* @event load
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event Window: load event}
*/
window.addEventListener("load", (event) => {
init();
});