├── .DS_Store
├── .gitattributes
├── .gitignore
├── README.md
├── index.html
├── main.js
├── modules
├── VRSupport.js
├── audioGuide.js
├── bench.js
├── boundingBox.js
├── ceiling.js
├── ceilingLamp.js
├── clickHandling.js
├── eventListeners.js
├── floor.js
├── lighting.js
├── menu.js
├── movement.js
├── paintingData.js
├── paintingInfo.js
├── paintings.js
├── rendering.js
├── scene.js
├── sceneHelpers.js
├── statue.js
└── walls.js
├── package-lock.json
├── package.json
├── public
├── .DS_Store
├── OfficeCeiling005_4K-JPG
│ ├── OfficeCeiling005_4K-JPG.usda
│ ├── OfficeCeiling005_4K-JPG.usdc
│ ├── OfficeCeiling005_4K_AmbientOcclusion.jpg
│ ├── OfficeCeiling005_4K_Color.jpg
│ ├── OfficeCeiling005_4K_Displacement.jpg
│ ├── OfficeCeiling005_4K_Emission.jpg
│ ├── OfficeCeiling005_4K_Metalness.jpg
│ ├── OfficeCeiling005_4K_NormalDX.jpg
│ ├── OfficeCeiling005_4K_NormalGL.jpg
│ ├── OfficeCeiling005_4K_Roughness.jpg
│ └── OfficeCeiling005_PREVIEW.jpg
├── WoodFloor040_4K-JPG
│ ├── WoodFloor040_4K-JPG.usda
│ ├── WoodFloor040_4K-JPG.usdc
│ ├── WoodFloor040_4K_AmbientOcclusion.jpg
│ ├── WoodFloor040_4K_Color.jpg
│ ├── WoodFloor040_4K_Displacement.jpg
│ ├── WoodFloor040_4K_NormalDX.jpg
│ ├── WoodFloor040_4K_NormalGL.jpg
│ ├── WoodFloor040_4K_Roughness.jpg
│ └── WoodFloor040_PREVIEW.jpg
├── artworks
│ ├── 0.jpg
│ ├── 1.jpg
│ ├── 10.jpg
│ ├── 11.jpg
│ ├── 12.jpg
│ ├── 13.jpg
│ ├── 14.jpg
│ ├── 15.jpg
│ ├── 16.jpg
│ ├── 17.jpg
│ ├── 18.jpg
│ ├── 19.jpg
│ ├── 2.jpg
│ ├── 20.jpg
│ ├── 21.jpg
│ ├── 22.jpg
│ ├── 23.jpg
│ ├── 24.jpg
│ ├── 25.jpg
│ ├── 26.jpg
│ ├── 27.jpg
│ ├── 28.jpg
│ ├── 29.jpg
│ ├── 3.jpg
│ ├── 4.jpg
│ ├── 5.jpg
│ ├── 6.jpg
│ ├── 7.jpg
│ ├── 8.jpg
│ └── 9.jpg
├── img
│ ├── Floor.jpg
│ ├── brick-wall-painted-white.jpg
│ ├── ceiling.jpg
│ ├── concrete-wall.jpg
│ ├── floor.png
│ ├── frame.jpg
│ ├── glass-background-with-grid-pattern.jpg
│ ├── light-gray-concrete-wall.jpg
│ ├── starrynight.jpg
│ ├── wall.jpg
│ └── white-texture.jpg
├── leather_white_4k.gltf
│ ├── .DS_Store
│ ├── leather_white.bin
│ ├── leather_white_4k.gltf
│ └── textures
│ │ ├── leather_white_diff_4k.jpg
│ │ ├── leather_white_nor_gl_4k.jpg
│ │ └── leather_white_rough_4k.jpg
├── models
│ ├── .DS_Store
│ ├── bench
│ │ ├── license.txt
│ │ ├── scene.bin
│ │ ├── scene.gltf
│ │ └── textures
│ │ │ ├── Stainless_Steel_diffuse.jpeg
│ │ │ ├── Stainless_Steel_normal.jpeg
│ │ │ ├── Stainless_Steel_specularGlossiness.png
│ │ │ ├── Wood_diffuse.jpeg
│ │ │ ├── Wood_normal.jpeg
│ │ │ └── Wood_specularGlossiness.png
│ ├── bench_2
│ │ ├── license.txt
│ │ ├── scene.bin
│ │ ├── scene.gltf
│ │ └── textures
│ │ │ ├── Luca3D_Modern_Bench_2_baseColor.png
│ │ │ ├── Luca3D_Modern_Bench_2_metallicRoughness.png
│ │ │ └── Luca3D_Modern_Bench_2_normal.png
│ ├── ceiling-lamp
│ │ ├── license.txt
│ │ ├── scene.bin
│ │ ├── scene.gltf
│ │ └── textures
│ │ │ └── Madeira_baseColor.jpeg
│ └── statue
│ │ ├── license.txt
│ │ ├── scene.bin
│ │ └── scene.gltf
└── sounds
│ └── tiersen.mp3
├── style.css
└── vite.config.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theringsofsaturn/3D-art-gallery-threejs/8be5adddaf60ac7f78f0a7f1fb28adbd84e98592/.DS_Store
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.psd filter=lfs diff=lfs merge=lfs -text
2 | *.jpg filter=lfs diff=lfs merge=lfs -text
3 | *.png filter=lfs diff=lfs merge=lfs -text
4 | *.gltf filter=lfs diff=lfs merge=lfs -text
5 | *.bin filter=lfs diff=lfs merge=lfs -text
6 | *.glb filter=lfs diff=lfs merge=lfs -text
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependency directories
2 | node_modules/
3 | jspm_packages/
4 |
5 | # dotenv environment variable files
6 | .env
7 | .env.development.local
8 | .env.test.local
9 | .env.production.local
10 | .env.local
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 3D Art Gallery Tutorial using Three.js
2 |
3 | I made this live coding tutorial about "how to create an interactive 3D Art Gallery using Three.js". This project is perfect for artists or designers to exhibit their artwork portfolios or projects. The full tutorial is almost 8 hours long, and is divided into parts. Please consider subscribing to my YouTube channel if you are interested.
4 |
5 | ## HELP!
6 | First of all, use GitHub commits to mark our progress, so you can easily follow along and see the project evolve. So, to use the initial exact Three.js module I am using, go to "Commits" section of the GitHub repository and scroll down to the first commits, and download that version of the code, then step by step review and compare the other versions of the code.
7 | If you don't know how to do that, follow these instructions:
8 |
9 | Go to my repository, click "commits", you will see all the commits list. Then scroll down to the first commit. On the right side of each commit name, you will see three small icons:
10 | - a code number --> which if you hover on it says "view commit details".
11 | - a two square icon --> which if you hover says "view full SHA"
12 | - '< >' this icon --> which if you hover on says "browse repository at this point".
13 |
14 | Click this last icon I mentioned, '< >'. You will be redirected to the exact point in time of this project. You can then download the repository as you normally do with the green button "Code".
15 |
16 | ## UPDATE!
17 |
18 | Dear followers and enthusiasts,
19 |
20 | I've been made aware of an issue many of you faced regarding the floor and ceiling textures appearing black. After a thorough investigation, I've identified the root of the problem. The high-resolution 4K textures we recently introduced are relatively large files. To manage such large files, GitHub uses a system called Large File Storage (LFS). However, there's a storage quota associated with LFS, and it seems we've reached that limit. This led to the textures not being stored correctly, resulting in broken image links in the downloaded projects.
21 |
22 | The solution at the moment:
23 | Download the 4K textures (or 2K /1K for better performance. I am currently using the 1K textures) and the 3D models yourself and add them in your project woth the correct path.
24 |
25 | The Office Ceiling material in 4K:
26 | https://ambientcg.com/view?id=OfficeCeiling005
27 |
28 | The Wood Floor in 4K:
29 | https://ambientcg.com/view?id=WoodFloor040
30 |
31 | The Walls in 4K:
32 | https://polyhaven.com/a/leather_white
33 |
34 | 3D Model Statue:
35 | https://sketchfab.com/3d-models/100kz11-aphrodite-kallipygos-statuette-c01ba617ec83491195146583b70e3df9
36 |
37 | ## Installation
38 |
39 | You need Node.js installed on your computer.
40 | And VSCode as an Editor.
41 | Download link:
42 |
43 | - https://nodejs.org
44 | - https://code.visualstudio.com/Download
45 |
46 | After cloning, or downloading the zip file, on GitHub (green button `<> Code`) open your terminal, and run:
47 |
48 | ```bash
49 | npm install
50 | ```
51 |
52 | to install all the dependencies.
53 | "node_modules" folder will appear at the left in the Explorer files in VsCode.
54 |
55 | Then run:
56 |
57 | ```bash
58 | npx vite
59 | ```
60 |
61 | to run the local server.
62 | You'll see the URL address and the info help. Like for example:
63 |
64 | ```bash
65 | VITE v4.3.1 ready in 1759 ms
66 |
67 | ➜ Local: http://123.4.5.6:7890/
68 | ➜ Network: use --host to expose ➜ press h to show help
69 | ```
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | ## YouTube Video
84 |
85 | [Click here!](https://youtu.be/vfMizAmPprs)
86 |
87 | ## Authors
88 |
89 | - [Emilian Kasemi](https://www.github.com/theringsofsaturn)
90 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Welcome to an immersive 3D Art Gallery experience.
23 |
24 | Wander through the virtual gallery halls, and engage with a
25 | variety of masterpieces, learning about their history and
26 | significance.
27 |
28 |
29 | Developed by Emilian Kasemi with a passion for bringing art to
30 | life through technology.
31 |
32 |
33 |
34 |
35 |
EXPLORE ART
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
About the 3D Art Gallery
47 |
48 | Welcome to our virtual 3D Art Gallery. Here, you can navigate through a
49 | variety of wonderful artworks and learn about their history and
50 | significance.
51 |
52 |
53 | This project is brought to you by Emilian Kasemi. We believe in the
54 | power of art to inspire, provoke thought, and communicate across
55 | cultures and epochs. Our mission is to make art accessible to everyone,
56 | regardless of location or background.
57 |
58 |
59 | Feel free to explore and engage with the artworks. The gallery is best
60 | experienced in full-screen mode. For optimal navigation, use the arrow
61 | keys or the W, A, S, and D keys. You can also use your mouse to change
62 | the viewing direction. Additional information about the artworks will
63 | appear on your screen as you approach them.
64 |
65 |
66 |
67 |
68 |
69 |
Controls
70 |
71 |
72 |
73 |
W/A/S/D: Move around
74 |
Mouse: Look around
75 |
Space: Toggle pointer lock
76 |
M: Show Menu
77 |
Enter: Start exploration
78 |
Esc: Stop exploration
79 |
G: Start audio guide
80 |
P: Stop audio guide
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | import { scene, setupScene } from "./modules/scene.js";
3 | import { createPaintings } from "./modules/paintings.js";
4 | import { createWalls } from "./modules/walls.js";
5 | import { setupLighting } from "./modules/lighting.js";
6 | import { setupFloor } from "./modules/floor.js";
7 | import { createCeiling } from "./modules/ceiling.js";
8 | import { createBoundingBoxes } from "./modules/boundingBox.js";
9 | import { setupRendering } from "./modules/rendering.js";
10 | import { setupEventListeners } from "./modules/eventListeners.js";
11 | import { addObjectsToScene } from "./modules/sceneHelpers.js";
12 | import { setupPlayButton } from "./modules/menu.js";
13 | import { setupAudio } from "./modules/audioGuide.js";
14 | import { clickHandling } from "./modules/clickHandling.js";
15 | import { setupVR } from "./modules/VRSupport.js";
16 | import { loadStatueModel } from "./modules/statue.js";
17 | import { loadBenchModel } from "./modules/bench.js";
18 | import { loadCeilingLampModel } from "./modules/ceilingLamp.js";
19 |
20 | let { camera, controls, renderer } = setupScene();
21 |
22 | setupAudio(camera);
23 |
24 | const textureLoader = new THREE.TextureLoader();
25 |
26 | const walls = createWalls(scene, textureLoader);
27 | const floor = setupFloor(scene);
28 | const ceiling = createCeiling(scene, textureLoader);
29 | const paintings = createPaintings(scene, textureLoader);
30 | const lighting = setupLighting(scene, paintings);
31 |
32 | createBoundingBoxes(walls);
33 | createBoundingBoxes(paintings);
34 |
35 | addObjectsToScene(scene, paintings);
36 |
37 | setupPlayButton(controls);
38 |
39 | setupEventListeners(controls);
40 |
41 | clickHandling(renderer, camera, paintings);
42 |
43 | setupRendering(scene, camera, renderer, paintings, controls, walls);
44 |
45 | loadStatueModel(scene);
46 |
47 | loadBenchModel(scene);
48 |
49 | loadCeilingLampModel(scene);
50 |
51 | setupVR(renderer);
52 |
--------------------------------------------------------------------------------
/modules/VRSupport.js:
--------------------------------------------------------------------------------
1 | import { VRButton } from "three/examples/jsm/webxr/VRButton.js";
2 |
3 | export const setupVR = (renderer) => {
4 | renderer.xr.enabled = true;
5 |
6 | renderer.xr.addEventListener("sessionstart", () => {
7 | console.log("WebXR session started");
8 | });
9 |
10 | renderer.xr.addEventListener("sessionend", () => {
11 | console.log("WebXR session ended");
12 | });
13 |
14 | document.body.appendChild(VRButton.createButton(renderer));
15 | };
16 |
--------------------------------------------------------------------------------
/modules/audioGuide.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 |
3 | let sound;
4 | let bufferLoaded = false; // flag to track if audio buffer is loaded
5 |
6 | // setup audio for the scene
7 | export const setupAudio = (camera) => {
8 | // create an audio listener and add it to the camera
9 | const listener = new THREE.AudioListener();
10 | camera.add(listener);
11 |
12 | sound = new THREE.Audio(listener); // creating the audio source
13 |
14 | const audioLoader = new THREE.AudioLoader(); // create an audio loader
15 | audioLoader.load("sounds/tiersen.mp3", function (buffer) {
16 | // load the audio file
17 | sound.setBuffer(buffer); // set the audio source buffer
18 | sound.setLoop(true); // set the audio source to loop
19 | sound.setVolume(0.5); // set the audio source volume
20 | bufferLoaded = true; // set bufferLoaded flag to true once the audio buffer is loaded
21 | });
22 | };
23 |
24 | // play audio
25 | export const startAudio = () => {
26 | if (sound && bufferLoaded) {
27 | // check if the buffer is loaded before playing
28 | sound.play();
29 | }
30 | };
31 |
32 | // pause audio
33 | export const stopAudio = () => {
34 | if (sound) {
35 | sound.pause();
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/modules/bench.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
3 | import { GUI } from "lil-gui";
4 |
5 | export const loadBenchModel = (scene) => {
6 | const loader = new GLTFLoader();
7 | const gui = new GUI();
8 |
9 | loader.load("../public/models/bench_2/scene.gltf", (gltf) => {
10 | const bench = gltf.scene;
11 | console.log("BENCH", gltf);
12 |
13 | // Iterate through all the meshes in the bench and update their materials
14 | bench.traverse((child) => {
15 | if (child.isMesh) {
16 | console.log("Materials:", child.material);
17 | console.log("Map Material", child.material.map);
18 | console.log("Material Name:", child.material.name);
19 | console.log("Material Type:", child.material.type);
20 | console.log("UV attributes:", child.geometry.attributes.uv);
21 | }
22 | undefined,
23 | (error) => {
24 | console.error(
25 | "An error occurred while loading the bench model.",
26 | error
27 | );
28 | };
29 | });
30 |
31 | // Default Position and Scale
32 | bench.position.set(0, -3.12, -8);
33 | bench.rotation.set(0, 0, 0);
34 | bench.scale.set(3, 3, 3);
35 |
36 | // Add the bench to the scene
37 | scene.add(bench);
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/modules/boundingBox.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | // check if objects is an array. If it's not, we assume it's a THREE.Group and set objects to objects.children. We then use forEach to loop over each object in objects and add a bounding box to it
4 | export const createBoundingBoxes = (objects) => {
5 | // objects will be either paintings or walls that we pass in from main.js
6 | if (!Array.isArray(objects)) {
7 | objects = objects.children;
8 | }
9 |
10 | objects.forEach((object) => {
11 | object.BoundingBox = new THREE.Box3(); // create a new bounding box for each object
12 | object.BoundingBox.setFromObject(object); // set the bounding box to the object (painting or wall)
13 | });
14 | };
15 |
16 | // Note: Without the checking won't work!
17 | // if (!Array.isArray(objects)) {
18 | // objects = objects.children;
19 | // }
20 |
21 | // Checking for both THREE.Group and arrays is because the `createWalls` function is returning a THREE.Group while `createPaintings` is returning an array.
22 |
23 | // In Three.js, a THREE.Group is a type of object used to create hierarchical (parent-child) relationships between objects. In other words, a Group is an object that can contain other objects. This is useful if we want to manipulate several objects as one.
24 |
25 | // The children property of a Group is an array that contains all the objects (child objects) that have been added to the group. So when we add an object to a group with group.add(object), we can access that object later with group.children.
26 |
27 | // In our code, createWalls returns a Group that contains several wall objects. When we pass this group to createBoundingBoxes, we want to create a bounding box for each wall in the group. To do that, we need to loop over the children array of the group.
28 |
29 | // So, when we call createBoundingBoxes(walls), the objects parameter of createBoundingBoxes is a Group object. In order to loop over each wall in the group, we need to set objects to objects.children.
30 |
31 | // However, createPaintings returns an array, not a Group. When you call createBoundingBoxes(paintings), the objects parameter of createBoundingBoxes is already an array, so you don't need to do anything.
32 |
33 | // That's why createBoundingBoxes starts by checking if objects is an array with if (!Array.isArray(objects)). If objects is not an array, it assumes that it's a Group and sets objects to objects.children. If objects is already an array, it skips this step.
34 |
35 | // After this check, objects is always an array, whether it was originally an array or a Group. This means that you can use objects.forEach to loop over each object in objects and add a bounding box to it, regardless of whether objects was originally an array or a Group. This is a way of making createBoundingBoxes flexible so it can work with both Groups and arrays.
36 |
--------------------------------------------------------------------------------
/modules/ceiling.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 |
3 | // create a function that takes a scene and a textureLoader as arguments that will be passed in from main.js where the createCeiling is called
4 | export const createCeiling = (scene, textureLoader) => {
5 | // Load the textures
6 | const colorTexture = textureLoader.load(
7 | "OfficeCeiling005_4K-JPG/OfficeCeiling005_4K_Color.jpg"
8 | );
9 | const displacementTexture = textureLoader.load(
10 | "OfficeCeiling005_4K-JPG/OfficeCeiling005_4K_Displacement.jpg"
11 | );
12 | const aoTexture = textureLoader.load(
13 | "OfficeCeiling005_4K-JPG/OfficeCeiling005_4K_AmbientOcclusion.jpg"
14 | );
15 | const emissionTexture = textureLoader.load(
16 | "OfficeCeiling005_4K-JPG/OfficeCeiling005_4K_Emission.jpg"
17 | );
18 | const metalnessTexture = textureLoader.load(
19 | "OfficeCeiling005_4K-JPG/OfficeCeiling005_4K_Metalness.jpg"
20 | );
21 | const normalGLTexture = textureLoader.load(
22 | "OfficeCeiling005_4K-JPG/OfficeCeiling005_4K_NormalGL.jpg"
23 | );
24 | const roughnessTexture = textureLoader.load(
25 | "OfficeCeiling005_4K-JPG/OfficeCeiling005_4K_Roughness.jpg"
26 | );
27 |
28 | // Set texture parameters
29 | colorTexture.wrapS = colorTexture.wrapT = THREE.RepeatWrapping;
30 | displacementTexture.wrapS = displacementTexture.wrapT = THREE.RepeatWrapping;
31 | aoTexture.wrapS = aoTexture.wrapT = THREE.RepeatWrapping;
32 | emissionTexture.wrapS = emissionTexture.wrapT = THREE.RepeatWrapping;
33 | metalnessTexture.wrapS = metalnessTexture.wrapT = THREE.RepeatWrapping;
34 | normalGLTexture.wrapS = normalGLTexture.wrapT = THREE.RepeatWrapping;
35 | roughnessTexture.wrapS = roughnessTexture.wrapT = THREE.RepeatWrapping;
36 |
37 | const ceilingGeometry = new THREE.PlaneGeometry(45, 40);
38 | const ceilingMaterial = new THREE.MeshLambertMaterial({
39 | map: colorTexture,
40 | displacementMap: displacementTexture,
41 | aoMap: aoTexture,
42 | emissiveMap: emissionTexture,
43 | metalnessMap: metalnessTexture,
44 | normalMap: normalGLTexture,
45 | normalMapType: THREE.NormalMap,
46 | roughnessMap: roughnessTexture,
47 | displacementScale: 0.1,
48 | side: THREE.DoubleSide,
49 | });
50 | const ceilingPlane = new THREE.Mesh(ceilingGeometry, ceilingMaterial);
51 |
52 | ceilingPlane.rotation.x = Math.PI / 2;
53 |
54 | ceilingPlane.position.y = 10;
55 |
56 | scene.add(ceilingPlane);
57 | };
58 |
--------------------------------------------------------------------------------
/modules/ceilingLamp.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
3 | import { GUI } from "lil-gui";
4 |
5 | export const loadCeilingLampModel = (scene) => {
6 | const loader = new GLTFLoader();
7 | const gui = new GUI();
8 |
9 | loader.load("../public/models/ceiling-lamp/scene.gltf", (gltf) => {
10 | const lamp = gltf.scene;
11 |
12 | console.log("Ceiling Lamp", gltf);
13 |
14 | // Position the lamp
15 | lamp.position.set(0, 5.5, 0);
16 | lamp.scale.set(0.1, 0.1, 0.1);
17 |
18 | // Add the lamp to the scene
19 | scene.add(lamp);
20 |
21 | // Add GUI controls for the lamp
22 | const lampFolder = gui.addFolder("Ceiling Lamp");
23 | lampFolder.add(lamp.position, "x", -50, 50).name("X Position");
24 | lampFolder.add(lamp.position, "y", -50, 50).name("Y Position");
25 | lampFolder.add(lamp.position, "z", -50, 50).name("Z Position");
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/modules/clickHandling.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | const mouse = new THREE.Vector2();
4 | const raycaster = new THREE.Raycaster();
5 |
6 | function clickHandling(renderer, camera, paintings) {
7 | renderer.domElement.addEventListener(
8 | 'click',
9 | (event) => {
10 | mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
11 | mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
12 | onClick(camera, paintings);
13 | },
14 | false
15 | );
16 | }
17 |
18 | function onClick(camera, paintings) {
19 | raycaster.setFromCamera(mouse, camera);
20 |
21 | const intersects = raycaster.intersectObjects(paintings);
22 |
23 | if (intersects.length > 0) {
24 | const painting = intersects[0].object;
25 |
26 | // Perform the desired action, e.g., open a modal or redirect to another page
27 | console.log('Clicked painting:', painting.userData.info.title);
28 | window.open(painting.userData.info.link, '_blank');
29 | }
30 | }
31 |
32 | export { clickHandling };
33 |
--------------------------------------------------------------------------------
/modules/eventListeners.js:
--------------------------------------------------------------------------------
1 | import { keysPressed } from "./movement.js"; // import the keysPressed object
2 | import { showMenu, hideMenu } from "./menu.js"; // import the showMenu function
3 | import { startAudio, stopAudio } from "./audioGuide.js";
4 |
5 | let lockPointer = true;
6 | let showMenuOnUnlock = false;
7 |
8 | // add the controls parameter which is the pointer lock controls and is passed from main.js where setupEventListeners is called
9 | export const setupEventListeners = (controls, camera, scene) => {
10 | // add the event listeners to the document which is the whole page
11 | document.addEventListener(
12 | "keydown",
13 | (event) => onKeyDown(event, controls),
14 | false
15 | );
16 | document.addEventListener(
17 | "keyup",
18 | (event) => onKeyUp(event, controls),
19 | false
20 | );
21 |
22 | controls.addEventListener("unlock", () => {
23 | if (showMenuOnUnlock) {
24 | showMenu();
25 | }
26 | showMenuOnUnlock = false;
27 | });
28 |
29 | // Add event listeners for the audio guide buttons
30 | document.getElementById("start_audio").addEventListener("click", startAudio);
31 | document.getElementById("stop_audio").addEventListener("click", stopAudio);
32 | };
33 |
34 | // toggle the pointer lock
35 | function togglePointerLock(controls) {
36 | if (lockPointer) {
37 | controls.lock();
38 | } else {
39 | showMenuOnUnlock = false;
40 | controls.unlock();
41 | }
42 | lockPointer = !lockPointer; // toggle the lockPointer variable
43 | }
44 |
45 | function onKeyDown(event, controls) {
46 | // event is the event object that has the key property
47 | if (event.key in keysPressed) {
48 | // check if the key pressed by the user is in the keysPressed object
49 | keysPressed[event.key] = true; // if yes, set the value of the key pressed to true
50 | }
51 |
52 | if (event.key === "Escape") {
53 | // if the "ESC" key is pressed
54 | showMenu(); // show the menu
55 | showMenuOnUnlock = true;
56 | controls.unlock(); // unlock the pointer
57 | lockPointer = false;
58 | }
59 |
60 | if (event.key === "p") {
61 | // if the "SPACE" key is pressed
62 | controls.unlock(); // unlock the pointer
63 | lockPointer = false;
64 | }
65 |
66 | // if key prssed is enter or return for mac
67 | if (event.key === "Enter" || event.key === "Return") {
68 | // if the "ENTER" key is pressed
69 | hideMenu(); // hide the menu
70 | controls.lock(); // lock the pointer
71 | lockPointer = true;
72 | }
73 |
74 | if (event.key === " ") {
75 | // if the "p" key is pressed
76 | togglePointerLock(controls); // toggle the pointer lock
77 | }
78 |
79 | if (event.key === "g") {
80 | // if the "a" key is pressed
81 | startAudio(); // start the audio guide
82 | }
83 |
84 | if (event.key === "p") {
85 | // if the "s" key is pressed
86 | stopAudio(); // stop the audio guide
87 | }
88 |
89 | if (event.key === "m") {
90 | // if the "h" key is pressed
91 | showMenu(); // show the menu
92 | showMenuOnUnlock = true;
93 | controls.unlock(); // unlock the pointer
94 | lockPointer = false;
95 | }
96 |
97 | if (event.key === "r") {
98 | // if the "r" key is pressed
99 | location.reload(); // reload the page
100 | }
101 | }
102 |
103 | function onKeyUp(event, controls) {
104 | // same but for keyup
105 | if (event.key in keysPressed) {
106 | keysPressed[event.key] = false; // set to false when the key is released
107 | }
108 | }
109 |
110 | document.getElementById("toggle-info").addEventListener("click", () => {
111 | document.getElementById("info-panel").classList.toggle("collapsed");
112 | document.getElementById("toggle-info").innerText = document
113 | .getElementById("info-panel")
114 | .classList.contains("collapsed")
115 | ? "Show"
116 | : "Hide";
117 | });
118 |
119 | document.getElementById("about_button").addEventListener("click", function () {
120 | document.getElementById("about-overlay").classList.add("show");
121 | });
122 |
123 | document.getElementById("close-about").addEventListener("click", function () {
124 | document.getElementById("about-overlay").classList.remove("show");
125 | });
126 |
--------------------------------------------------------------------------------
/modules/floor.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | import floortTexture from "../public/WoodFloor040_4K-JPG/WoodFloor040_4K_Color.jpg";
3 |
4 | export const setupFloor = (scene) => {
5 | const textureLoader = new THREE.TextureLoader();
6 |
7 | // Load the textures
8 | const colorTexture = textureLoader.load(
9 | "WoodFloor040_4K-JPG/WoodFloor040_4K_Color.jpg"
10 | );
11 | const displacementTexture = textureLoader.load(
12 | "WoodFloor040_4K-JPG/WoodFloor040_4K_Displacement.jpg"
13 | );
14 | const normalTexture = textureLoader.load(
15 | "WoodFloor040_4K-JPG/WoodFloor040_4K_NormalGL.jpg"
16 | );
17 | const roughnessTexture = textureLoader.load(
18 | "WoodFloor040_4K-JPG/WoodFloor040_4K_Roughness.jpg"
19 | );
20 | const aoTexture = textureLoader.load(
21 | "WoodFloor040_4K-JPG/WoodFloor040_4K_AmbientOcclusion.jpg"
22 | );
23 |
24 | // Set texture parameters
25 | colorTexture.wrapS = colorTexture.wrapT = THREE.RepeatWrapping;
26 | displacementTexture.wrapS = displacementTexture.wrapT = THREE.RepeatWrapping;
27 | normalTexture.wrapS = normalTexture.wrapT = THREE.RepeatWrapping;
28 | roughnessTexture.wrapS = roughnessTexture.wrapT = THREE.RepeatWrapping;
29 | aoTexture.wrapS = aoTexture.wrapT = THREE.RepeatWrapping;
30 |
31 | const planeGeometry = new THREE.PlaneGeometry(45, 45);
32 | const planeMaterial = new THREE.MeshStandardMaterial({
33 | map: colorTexture,
34 | displacementMap: displacementTexture,
35 | normalMap: normalTexture,
36 | roughnessMap: roughnessTexture,
37 | aoMap: aoTexture,
38 | displacementScale: 0.1,
39 | side: THREE.DoubleSide,
40 | });
41 |
42 | const floorPlane = new THREE.Mesh(planeGeometry, planeMaterial);
43 |
44 | floorPlane.rotation.x = Math.PI / 2;
45 | floorPlane.position.y = -Math.PI;
46 |
47 | scene.add(floorPlane);
48 | };
49 |
--------------------------------------------------------------------------------
/modules/lighting.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | import { GUI } from "lil-gui";
3 |
4 | export const setupLighting = (scene, paintings) => {
5 | // Initialize GUI
6 | const gui = new GUI();
7 |
8 | // Ambient light
9 | const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
10 | scene.add(ambientLight);
11 |
12 | // GUI for Ambient Light
13 | const ambientFolder = gui.addFolder("Ambient Light");
14 | ambientFolder.add(ambientLight, "intensity", 0, 2);
15 |
16 | function createSpotlight(x, y, z, intensity, targetPosition) {
17 | const spotlight = new THREE.SpotLight(0xffffff, intensity);
18 | spotlight.position.set(x, y, z);
19 | spotlight.target.position.copy(targetPosition);
20 | spotlight.castShadow = true;
21 | spotlight.angle = 1.57079;
22 | spotlight.penumbra = 0.2;
23 | spotlight.decay = 1;
24 | spotlight.distance = 40;
25 | spotlight.shadow.mapSize.width = 1024;
26 | spotlight.shadow.mapSize.height = 1024;
27 |
28 | // Add spotlight and its target to the scene
29 | scene.add(spotlight);
30 | scene.add(spotlight.target);
31 |
32 | // Add a helper for this spotlight
33 | // const spotlightHelper = new THREE.SpotLightHelper(spotlight);
34 | // scene.add(spotlightHelper);
35 |
36 | // Create a GUI folder for this spotlight
37 | const folder = gui.addFolder(`Spotlight (${x}, ${y}, ${z})`);
38 | folder.add(spotlight, "intensity", 0, 4);
39 | folder.add(spotlight, "angle", 0, Math.PI / 2).name("Angle");
40 | folder.add(spotlight, "penumbra", 0, 1).name("Penumbra");
41 | folder.add(spotlight, "decay", 0, 2).name("Decay");
42 | folder.add(spotlight, "distance", 0, 100).name("Distance");
43 | folder.add(spotlight.position, "x", -50, 50);
44 | folder.add(spotlight.position, "y", -50, 50);
45 | folder.add(spotlight.position, "z", -50, 50);
46 | folder.add(spotlight.target.position, "x", -50, 50);
47 | folder.add(spotlight.target.position, "y", -50, 50);
48 | folder.add(spotlight.target.position, "z", -50, 50);
49 |
50 | return spotlight;
51 | }
52 |
53 | const frontWallSpotlight = createSpotlight(
54 | 0,
55 | 6.7,
56 | -13,
57 | 0.948,
58 | new THREE.Vector3(0, 0, -20)
59 | );
60 |
61 | const backWallSpotlight = createSpotlight(
62 | 0,
63 | 6.7,
64 | 13,
65 | 0.948,
66 | new THREE.Vector3(0, 0, 20)
67 | );
68 |
69 | const leftWallSpotlight = createSpotlight(
70 | -13,
71 | 6.7,
72 | 0,
73 | 0.948,
74 | new THREE.Vector3(-20, 0, 0)
75 | );
76 |
77 | const rightWallSpotlight = createSpotlight(
78 | 13,
79 | 6.7,
80 | 0,
81 | 0.948,
82 | new THREE.Vector3(20, 0, 0)
83 | );
84 |
85 | const statueSpotlight = createSpotlight(
86 | 0,
87 | 10,
88 | 0,
89 | 0.948,
90 | new THREE.Vector3(0, -4.2, 0)
91 | ); // Spotlight for the statue
92 | statueSpotlight.angle = 0.75084;
93 | statueSpotlight.decay = 1;
94 | statueSpotlight.penumbra = 1;
95 | statueSpotlight.distance = 0;
96 |
97 | const statueSpotlightFolder = gui.addFolder("Statue Light");
98 | statueSpotlightFolder.add(statueSpotlight, "intensity", 0, 4);
99 | statueSpotlightFolder
100 | .add(statueSpotlight, "angle", 0, Math.PI / 2)
101 | .name("Angle");
102 | statueSpotlightFolder.add(statueSpotlight, "penumbra", 0, 1).name("Penumbra");
103 | statueSpotlightFolder.add(statueSpotlight, "decay", 0, 2).name("Decay");
104 | };
105 |
--------------------------------------------------------------------------------
/modules/menu.js:
--------------------------------------------------------------------------------
1 | export const hideMenu = () => {
2 | const menu = document.getElementById('menu');
3 | menu.style.display = 'none'; // Hide the menu
4 | };
5 |
6 | export const showMenu = () => {
7 | const menu = document.getElementById('menu');
8 | menu.style.display = 'block'; // Show the menu
9 | };
10 |
11 | // Lock the pointer (controls are activated) and hide the menu when the experience starts
12 | export const startExperience = (controls) => {
13 | controls.lock(); // Lock the pointer (controls are activated)
14 | hideMenu();
15 | };
16 |
17 | export const setupPlayButton = (controls) => {
18 | const playButton = document.getElementById('play_button'); // Get the reference
19 | playButton.addEventListener('click', () => startExperience(controls)); // Add the click event listener to the play button to start the experience
20 | };
21 |
--------------------------------------------------------------------------------
/modules/movement.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | // object to hold the keys pressed
4 | export const keysPressed = {
5 | ArrowUp: false,
6 | ArrowDown: false,
7 | ArrowLeft: false,
8 | ArrowRight: false,
9 | w: false,
10 | a: false,
11 | s: false,
12 | d: false,
13 | };
14 |
15 | // parameters we get from setupRendering where updateMovement is called. setupRendering gets the parameters from main.jsss
16 | export const updateMovement = (delta, controls, camera, walls) => {
17 | const moveSpeed = 5 * delta; // moveSpeed is the distance the camera will move in one second. We multiply by delta to make the movement framerate independent. This means that the movement will be the same regardless of the framerate. This is important because if the framerate is low, the movement will be slow and if the framerate is high, the movement will be fast. This is not what we want. We want the movement to be the same regardless of the framerate.
18 |
19 | const previousPosition = camera.position.clone(); // clone the camera position and store it in previousPosition. We will use this to reset the camera position if there is a collision
20 |
21 | // cose self-explanatory
22 | if (keysPressed.ArrowRight || keysPressed.d) {
23 | controls.moveRight(moveSpeed);
24 | }
25 | if (keysPressed.ArrowLeft || keysPressed.a) {
26 | controls.moveRight(-moveSpeed);
27 | }
28 | if (keysPressed.ArrowUp || keysPressed.w) {
29 | controls.moveForward(moveSpeed);
30 | }
31 | if (keysPressed.ArrowDown || keysPressed.s) {
32 | controls.moveForward(-moveSpeed);
33 | }
34 |
35 | // After the movement is applied, we check for collisions by calling the checkCollision function. If a collision is detected, we revert the camera's position to its previous position, effectively preventing the player from moving through walls.
36 | if (checkCollision(camera, walls)) {
37 | camera.position.copy(previousPosition); // reset the camera position to the previous position. The `previousPosition` variable is a clone of the camera position before the movement. We use `copy` instead of `set` because `set` will set the position to the same object, so if we change the previousPosition, the camera position will also change. `copy` creates a new object with the same values as the previousPosition.
38 | }
39 | };
40 |
41 | // checkCollision takes the camera and the walls as parameters and returns true if there is a collision and false if there isn't. the camera parameter is the camera object and the walls parameter is the walls group. The paramaters are passed from updateMovement function where checkCollision is called. updateMovement gets the parameters from setupRendering where it is called. setupRendering gets the parameters from main.js where setupRendering is called.
42 | export const checkCollision = (camera, walls) => {
43 | const playerBoundingBox = new THREE.Box3(); // create a bounding box for the player
44 | const cameraWorldPosition = new THREE.Vector3(); // create a vector to hold the camera's world position
45 | camera.getWorldPosition(cameraWorldPosition); // get the camera's world position and store it in cameraWorldPosition. Note: The camera represents the player's position in our case.
46 | playerBoundingBox.setFromCenterAndSize(
47 | // set the playerBoundingBox to the camera's world position and size. The size is 1, 1, 1 because the camera is a single point.
48 | // setFromCenterAndSize takes two parameters: center and size. The center is a Vector3 that represents the center of the bounding box. The size is a Vector3 that represents the size of the bounding box. The size is the distance from the center to the edge of the bounding box in each direction. So, if the size is 1, 1, 1, the bounding box will be 2 units wide, 2 units tall, and 2 units deep. If the size is 2, 2, 2, the bounding box will be 4 units wide, 4 units tall, and 4 units deep.
49 | cameraWorldPosition, // center
50 | new THREE.Vector3(1, 1, 1) // size
51 | );
52 |
53 | for (let i = 0; i < walls.children.length; i++) {
54 | // loop through each wall
55 | const wall = walls.children[i]; // get the wall
56 | if (playerBoundingBox.intersectsBox(wall.BoundingBox)) {
57 | // check if the playerBoundingBox intersects with the wall's bounding box. If it does, return true.
58 | return true;
59 | }
60 | }
61 |
62 | return false; // if the playerBoundingBox doesn't intersect with any of the walls, return false.
63 | };
64 |
--------------------------------------------------------------------------------
/modules/paintingData.js:
--------------------------------------------------------------------------------
1 | export const paintingData = [
2 | // Front Wall
3 | ...Array.from({ length: 4 }, (_, i) => ({
4 | // Array.from creates an array from an array-like object. The first parameter is the array-like object. The second parameter is a map function that is called for each element in the array-like object. The map function takes two parameters: the element and the index. The map function returns the value that will be added to the new array. In this case, we are returning an object with the painting data. `_` is a placeholder for the element. We don't need it because we are not using the element. `i` is the index. We use it to set the painting number.
5 | imgSrc: `artworks/${i + 1}.jpg`, // `i + 1` is the painting number. We add 1 to the index because the index starts at 0 but we want the painting numbers to start at 1.
6 | width: 5, // width of the painting
7 | height: 3, // height of the painting
8 | position: { x: -15 + 10 * i, y: 2, z: -19.5 }, // position of the painting
9 | rotationY: 0, // rotation of the painting
10 | info: {
11 | // info about the painting
12 | title: `Van Gogh ${i + 1}`,
13 | artist: 'Vincent van Gogh',
14 | description: `This is one of the masterpieces by Vincent van Gogh, showcasing his unique style and emotional honesty. Artwork ${
15 | i + 1
16 | } perfectly encapsulates his love for the beauty of everyday life.`,
17 | year: `Year ${i + 1}`,
18 | link: 'https://github.com/theringsofsaturn',
19 | },
20 | })),
21 | // Back Wall
22 | ...Array.from({ length: 4 }, (_, i) => ({
23 | imgSrc: `artworks/${i + 5}.jpg`,
24 | width: 5,
25 | height: 3,
26 | position: { x: -15 + 10 * i, y: 2, z: 19.5 },
27 | rotationY: Math.PI,
28 | info: {
29 | title: `Van Gogh ${i + 5}`,
30 | artist: 'Vincent van Gogh',
31 | description: `Artwork ${
32 | i + 5
33 | } by Vincent van Gogh is an exceptional piece showcasing his remarkable ability to capture emotion and atmosphere.`,
34 | year: `Year ${i + 5}`,
35 | link: 'https://github.com/theringsofsaturn',
36 | },
37 | })),
38 | // Left Wall
39 | ...Array.from({ length: 4 }, (_, i) => ({
40 | imgSrc: `artworks/${i + 9}.jpg`,
41 | width: 5,
42 | height: 3,
43 | position: { x: -19.5, y: 2, z: -15 + 10 * i },
44 | rotationY: Math.PI / 2,
45 | info: {
46 | title: `Van Gogh ${i + 9}`,
47 | artist: 'Vincent van Gogh',
48 | description: `With its striking use of color and brushwork, Artwork ${
49 | i + 9
50 | } is a testament to Van Gogh's artistic genius.`,
51 | year: `Year ${i + 9}`,
52 | link: 'https://github.com/theringsofsaturn',
53 | },
54 | })),
55 | // Right Wall
56 | ...Array.from({ length: 4 }, (_, i) => ({
57 | imgSrc: `artworks/${i + 13}.jpg`,
58 | width: 5,
59 | height: 3,
60 | position: { x: 19.5, y: 2, z: -15 + 10 * i },
61 | rotationY: -Math.PI / 2,
62 | info: {
63 | title: `Van Gogh ${i + 13}`,
64 | artist: 'Vincent van Gogh',
65 | description: `Artwork ${
66 | i + 13
67 | } is a captivating piece by Vincent van Gogh, reflecting his distinctive style and deep passion for art.`,
68 | year: `Year ${i + 13}`,
69 | link: 'https://github.com/theringsofsaturn',
70 | },
71 | })),
72 | ];
73 |
--------------------------------------------------------------------------------
/modules/paintingInfo.js:
--------------------------------------------------------------------------------
1 | // Display painting info in the DOM
2 | export const displayPaintingInfo = (info) => {
3 | const infoElement = document.getElementById('painting-info'); // Get the reference
4 |
5 | // Set the html content inside info element
6 | infoElement.innerHTML = `
7 |
${info.title}
8 |
Artist: ${info.artist}
9 |
Description: ${info.description}
10 |
Year: ${info.year}
11 | `;
12 | infoElement.classList.add('show'); // Add the 'show' class
13 | };
14 |
15 | // Hide painting info in the DOM
16 | export const hidePaintingInfo = () => {
17 | const infoElement = document.getElementById('painting-info'); // Get the reference
18 | infoElement.classList.remove('show'); // Remove the 'show' class
19 | };
20 |
21 |
--------------------------------------------------------------------------------
/modules/paintings.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | import { paintingData } from './paintingData.js';
4 |
5 | export function createPaintings(scene, textureLoader) {
6 |
7 | let paintings = [];
8 |
9 | paintingData.forEach((data) => {
10 |
11 | const painting = new THREE.Mesh(
12 | new THREE.PlaneGeometry(data.width, data.height),
13 | new THREE.MeshLambertMaterial({ map: textureLoader.load(data.imgSrc) })
14 | );
15 |
16 | painting.position.set(data.position.x, data.position.y, data.position.z);
17 | painting.rotation.y = data.rotationY;
18 |
19 |
20 | painting.userData = {
21 | type: 'painting',
22 | info: data.info,
23 | url: data.info.link
24 | };
25 |
26 | painting.castShadow = true;
27 | painting.receiveShadow = true;
28 |
29 | paintings.push(painting);
30 | });
31 |
32 | return paintings;
33 | }
34 |
--------------------------------------------------------------------------------
/modules/rendering.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | import { displayPaintingInfo, hidePaintingInfo } from "./paintingInfo.js";
3 | import { updateMovement } from "./movement.js";
4 |
5 | export const setupRendering = (
6 | scene,
7 | camera,
8 | renderer,
9 | paintings,
10 | controls,
11 | walls
12 | ) => {
13 | const clock = new THREE.Clock();
14 |
15 | let render = function () {
16 | const delta = clock.getDelta();
17 |
18 | updateMovement(delta, controls, camera, walls);
19 |
20 | const distanceThreshold = 8;
21 |
22 | let paintingToShow;
23 | paintings.forEach((painting) => {
24 | const distanceToPainting = camera.position.distanceTo(painting.position);
25 | if (distanceToPainting < distanceThreshold) {
26 | paintingToShow = painting;
27 | }
28 | });
29 |
30 | if (paintingToShow) {
31 | // if there is a painting to show
32 | displayPaintingInfo(paintingToShow.userData.info);
33 | } else {
34 | hidePaintingInfo();
35 | }
36 |
37 | renderer.gammaOutput = true;
38 | renderer.gammaFactor = 2.2;
39 |
40 | renderer.render(scene, camera);
41 | requestAnimationFrame(render);
42 | };
43 |
44 | render();
45 | };
46 |
--------------------------------------------------------------------------------
/modules/scene.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | import { PointerLockControls } from "three-stdlib";
3 |
4 | export const scene = new THREE.Scene(); // create a scene
5 | let camera;
6 | let controls;
7 | let renderer;
8 |
9 | export const setupScene = () => {
10 | // PerspectiveCamera is a type of camera that mimics the way the human eye sees things. It takes 4 parameters: field of view, aspect ratio, near clipping plane, and far clipping plane. The field of view is the extent of the scene that is seen on the display at any given moment. The aspect ratio should be the width of the element divided by the height (in this case, the screen width and height). The camera will not render objects that are closer to the camera than the near clipping plane or further away than the far clipping plane. Objects that are exactly on the clipping plane will not be rendered.
11 | camera = new THREE.PerspectiveCamera(
12 | 60, // fov = field of view
13 | window.innerWidth / window.innerHeight, // aspect ratio
14 | 0.1, // near clipping plane
15 | 1000 // far clipping plane
16 | );
17 | scene.add(camera); // add the camera to the scene
18 | camera.position.set(0, 2, 15); // move the camera up 3 units in the Y axis
19 |
20 | renderer = new THREE.WebGLRenderer({ antialias: false }); // create a WebGLRenderer and set its antialias property to true to enable antialiasing which smooths out the edges of what is rendered
21 | renderer.setSize(window.innerWidth, window.innerHeight); // set the size of the renderer to the inner width and height of the window (the browser window)
22 | renderer.setClearColor(0xffffff, 1); // set the background color of the renderer to white
23 | document.body.appendChild(renderer.domElement); // append the renderer to the body of the document (the