├── .eslintrc.json
├── .gitignore
├── PlaneGameRecording.gif
├── README.md
├── Rishab Lamba.pdf
├── chair.gif
├── favicon.svg
├── index.html
├── landingpage.gif
├── libs
├── JoyStick.js
├── LoadingBar.js
├── Noise.js
├── SFX.js
└── Toon3D.js
├── main.js
├── package-lock.json
├── package.json
├── projects
├── custom-chairs
│ ├── index.html
│ ├── script.js
│ └── style.css
├── plane
│ ├── Explosion.js
│ ├── Game.js
│ ├── Obstacles.js
│ ├── Plane.js
│ ├── SFX.js
│ └── index.html
├── shootout
│ ├── BulletHandler.js
│ ├── Collisions.js
│ ├── Controller.js
│ ├── Game.js
│ ├── NPC.js
│ ├── NPCHandler.js
│ ├── UI.js
│ ├── User.js
│ ├── index.html
│ ├── pathfinding
│ │ ├── AStar.js
│ │ ├── BinaryHeap.js
│ │ ├── Builder.js
│ │ ├── Channel.js
│ │ ├── Pathfinding.js
│ │ ├── PathfindingHelper.js
│ │ ├── Utils.js
│ │ └── index.js
│ └── three128
│ │ ├── DRACOLoader.js
│ │ ├── GLTFLoader.js
│ │ ├── OrbitControls.js
│ │ ├── RGBELoader.js
│ │ ├── pp
│ │ ├── CopyShader.js
│ │ ├── EffectComposer.js
│ │ ├── GammaCorrectionShader.js
│ │ ├── MaskPass.js
│ │ ├── Pass.js
│ │ ├── RenderPass.js
│ │ └── ShaderPass.js
│ │ └── three.module.js
└── sphere
│ ├── index.html
│ ├── main.js
│ └── style.css
├── public
├── _redirects
├── assets
│ ├── draco
│ │ ├── README.md
│ │ ├── draco_decoder.js
│ │ ├── draco_decoder.wasm
│ │ ├── draco_encoder.js
│ │ ├── draco_wasm_wrapper.js
│ │ └── gltf
│ │ │ ├── draco_decoder.js
│ │ │ ├── draco_decoder.wasm
│ │ │ ├── draco_encoder.js
│ │ │ └── draco_wasm_wrapper.js
│ ├── factory
│ │ ├── ammo.svg
│ │ ├── eve-rifle.glb
│ │ ├── eve.glb
│ │ ├── eve2.glb
│ │ ├── factory1.glb
│ │ ├── factory2.glb
│ │ ├── gameover.ai
│ │ ├── gameover.png
│ │ ├── gameover.svg
│ │ ├── health.svg
│ │ ├── playagain.ai
│ │ ├── playagain.png
│ │ ├── playbtn.svg
│ │ ├── playgame.ai
│ │ ├── playgame.png
│ │ ├── sfx
│ │ │ ├── atmos.mp3
│ │ │ ├── eve-groan.mp3
│ │ │ ├── footsteps.mp3
│ │ │ ├── groan.mp3
│ │ │ └── shot.mp3
│ │ ├── sniper-rifle.glb
│ │ ├── swat-guy-rifle.glb
│ │ ├── swat-guy.glb
│ │ └── swat-guy2.glb
│ ├── hdr
│ │ ├── apartment.hdr
│ │ ├── factory.hdr
│ │ ├── field_sky.hdr
│ │ ├── living_room.hdr
│ │ └── venice_sunset_1k.hdr
│ ├── plane
│ │ ├── bomb.glb
│ │ ├── bonus.mp3
│ │ ├── engine.mp3
│ │ ├── explosion.mp3
│ │ ├── explosion.png
│ │ ├── gameover.mp3
│ │ ├── gliss.mp3
│ │ ├── microplane.glb
│ │ ├── paintedsky
│ │ │ ├── nx.jpg
│ │ │ ├── ny.jpg
│ │ │ ├── nz.jpg
│ │ │ ├── px.jpg
│ │ │ ├── py.jpg
│ │ │ └── pz.jpg
│ │ ├── plane-icon.png
│ │ ├── star-icon.png
│ │ └── star.glb
│ ├── pool-table
│ │ ├── 10ball.png
│ │ ├── 11ball.png
│ │ ├── 12ball.png
│ │ ├── 13ball.png
│ │ ├── 14ball.png
│ │ ├── 15ball.png
│ │ ├── 1ball.png
│ │ ├── 2ball.png
│ │ ├── 3ball.png
│ │ ├── 4ball.png
│ │ ├── 5ball.png
│ │ ├── 6ball.png
│ │ ├── 7ball.png
│ │ ├── 8ball.png
│ │ ├── 9ball.png
│ │ └── pool-table.glb
│ └── star-icon.png
└── img
│ ├── denim_.jpg
│ ├── fabric_.jpg
│ ├── pattern_.jpg
│ ├── quilt_.jpg
│ └── wood_.jpg
├── sphere.gif
├── style.css
└── vite.config.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": "eslint:recommended",
8 | "parserOptions": {
9 | "ecmaVersion": 12,
10 | "sourceType": "module"
11 | },
12 | "rules": {
13 | "no-unused-vars": "warn",
14 | "no-empty": "warn"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
--------------------------------------------------------------------------------
/PlaneGameRecording.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/PlaneGameRecording.gif
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Three JS Projects
2 | 3D Model Color Customizer AppClick here to design your own chair
3 |
4 |
Fully customizable with complex textures like wood, metal, silk etc
5 |
6 |
7 |
8 |
9 |
10 | #
11 |
Shootout (Not ready for production) Play an in-browser shooting game
12 |
13 |
14 |
15 |
16 | #
17 |
18 | Flappy PlaneClick here to play an in-browser game inspired by flappy birds
19 |
20 |
21 |
22 |
23 | #
24 |
25 | Sparkling WavesClick here to check out basically the landing page of my website
26 |
27 |
28 |
29 |
30 | #
31 |
32 | Feel The SphereClick here to play with an awesome looking metalic ball :P
33 |
34 |
35 |
36 |
37 | #
38 |
39 | Steps to Run the project
40 |
41 | - Run `npm install` in terminal to install the dependecies
42 | - Run `npm run dev` in terminal to start running the application
43 |
44 |
45 |
46 |
47 | Hey there! I'm Rishab Lamba.
48 |
49 |
50 | 👨🏻💻 About Me
51 |
52 | - 🔭 I’m a FullStack Developer with proficiency in the MERN(Mongo DB|Express|React|Node) stack and 3D UI development
53 | - 🌱 Enthusiast in Data Science and Artificial Intelligence .
54 | - 🎓 Pursuing my Masters in Computer Science from University of Illinois at Chicago.
55 | - 🤔 Exploring new technologies and developing software solutions and quick hacks.
56 | - 💼 Software Engineer.
57 | - ☕ I belive, a perfect cup of coffee can be the ultimate solution for any stress.
58 |
59 | 🛠 Tech Stack
60 |
61 | - 💻 JavaScript | Python | ReactJS | NodeJS | Express | Three JS
62 | - 🌐 React Native | HTML | CSS | JavaScript | Bootstrap
63 | - 🛢 Mongo DB | MySQL | Firebase
64 | - 🔧 Visual Studio code | Git
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | [](https://github.com/Rikki407/github-readme-stats)
73 |
74 | 🤝🏻 Connect with Me
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/Rishab Lamba.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/Rishab Lamba.pdf
--------------------------------------------------------------------------------
/chair.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/chair.gif
--------------------------------------------------------------------------------
/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 | Rishab Lamba
13 |
14 |
15 |
16 |
Rishab Lamba
17 |
HE WHO CLIMBS THE LADDER MUST BEGIN AT THE BOTTOM
18 |
SHOOT OUT
19 |
20 | 3D Model Color Customizer App
21 |
22 |
Flappy Plane
23 |
Sphere
24 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/landingpage.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/landingpage.gif
--------------------------------------------------------------------------------
/libs/JoyStick.js:
--------------------------------------------------------------------------------
1 | class JoyStick{
2 | constructor(options){
3 | const circle = document.createElement("div");
4 | if (options.left!==undefined){
5 | circle.style.cssText = "position:absolute; bottom:35px; width:80px; height:80px; background:rgba(126, 126, 126, 0.5); border:#444 solid medium; border-radius:50%; left:20px;";
6 | }else if (options.right!==undefined){
7 | circle.style.cssText = "position:absolute; bottom:35px; width:80px; height:80px; background:rgba(126, 126, 126, 0.5); border:#444 solid medium; border-radius:50%; right:20px;";
8 | }else{
9 | circle.style.cssText = "position:absolute; bottom:35px; width:80px; height:80px; background:rgba(126, 126, 126, 0.5); border:#444 solid medium; border-radius:50%; left:50%; transform:translateX(-50%);";
10 | }
11 | const thumb = document.createElement("div");
12 | thumb.style.cssText = "position: absolute; left: 20px; top: 20px; width: 40px; height: 40px; border-radius: 50%; background: #fff;";
13 | circle.appendChild(thumb);
14 | document.body.appendChild(circle);
15 | this.domBg = circle;
16 | this.domElement = thumb;
17 | this.maxRadius = options.maxRadius || 40;
18 | this.maxRadiusSquared = this.maxRadius * this.maxRadius;
19 | this.onMove = options.onMove;
20 | this.app = options.app;
21 | this.origin = { left:this.domElement.offsetLeft, top:this.domElement.offsetTop };
22 | this.rotationDamping = options.rotationDamping || 0.06;
23 | this.moveDamping = options.moveDamping || 0.01;
24 | if (this.domElement!=undefined){
25 | const joystick = this;
26 | if ('ontouchstart' in window){
27 | this.domElement.addEventListener('touchstart', function(evt){ evt.preventDefault(); joystick.tap(evt); });
28 | }else{
29 | this.domElement.addEventListener('mousedown', function(evt){ evt.preventDefault(); joystick.tap(evt); });
30 | }
31 | }
32 | }
33 |
34 | set visible( mode ){
35 | const setting = (mode) ? 'block' : 'none';
36 | this.domElement.style.display = setting;
37 | this.domBg.style.display = setting;
38 | }
39 |
40 | getMousePosition(evt){
41 | let clientX = evt.targetTouches ? evt.targetTouches[0].pageX : evt.clientX;
42 | let clientY = evt.targetTouches ? evt.targetTouches[0].pageY : evt.clientY;
43 | return { x:clientX, y:clientY };
44 | }
45 |
46 | tap(evt){
47 | evt = evt || window.event;
48 | // get the mouse cursor position at startup:
49 | this.offset = this.getMousePosition(evt);
50 | const joystick = this;
51 | if ('ontouchstart' in window){
52 | document.ontouchmove = function(evt){ evt.preventDefault(); joystick.move(evt); };
53 | document.ontouchend = function(evt){ evt.preventDefault(); joystick.up(evt); };
54 | }else{
55 | document.onmousemove = function(evt){ evt.preventDefault(); joystick.move(evt); };
56 | document.onmouseup = function(evt){ evt.preventDefault(); joystick.up(evt); };
57 | }
58 | }
59 |
60 | move(evt){
61 | evt = evt || window.event;
62 | const mouse = this.getMousePosition(evt);
63 | // calculate the new cursor position:
64 | let left = mouse.x - this.offset.x;
65 | let top = mouse.y - this.offset.y;
66 | //this.offset = mouse;
67 |
68 | const sqMag = left*left + top*top;
69 | if (sqMag>this.maxRadiusSquared){
70 | //Only use sqrt if essential
71 | const magnitude = Math.sqrt(sqMag);
72 | left /= magnitude;
73 | top /= magnitude;
74 | left *= this.maxRadius;
75 | top *= this.maxRadius;
76 | }
77 | // set the element's new position:
78 | this.domElement.style.top = `${top + this.domElement.clientHeight/2}px`;
79 | this.domElement.style.left = `${left + this.domElement.clientWidth/2}px`;
80 |
81 | const forward = -(top - this.origin.top + this.domElement.clientHeight/2)/this.maxRadius;
82 | const turn = (left - this.origin.left + this.domElement.clientWidth/2)/this.maxRadius;
83 |
84 | if (this.onMove!=undefined) this.onMove.call(this.app, forward, turn);
85 | }
86 |
87 | up(evt){
88 | if ('ontouchstart' in window){
89 | document.ontouchmove = null;
90 | document.touchend = null;
91 | }else{
92 | document.onmousemove = null;
93 | document.onmouseup = null;
94 | }
95 | this.domElement.style.top = `${this.origin.top}px`;
96 | this.domElement.style.left = `${this.origin.left}px`;
97 |
98 | this.onMove.call(this.app, 0, 0);
99 | }
100 | }
101 |
102 | export { JoyStick };
--------------------------------------------------------------------------------
/libs/LoadingBar.js:
--------------------------------------------------------------------------------
1 | class LoadingBar {
2 | constructor() {
3 | this.domElement = document.createElement('div');
4 | this.domElement.style.position = 'fixed';
5 | this.domElement.style.top = '0';
6 | this.domElement.style.left = '0';
7 | this.domElement.style.width = '100%';
8 | this.domElement.style.height = '100%';
9 | this.domElement.style.background = '#000';
10 | this.domElement.style.opacity = '0.7';
11 | this.domElement.style.display = 'flex';
12 | this.domElement.style.alignItems = 'center';
13 | this.domElement.style.justifyContent = 'center';
14 | this.domElement.style.zIndex = '1111';
15 | const barBase = document.createElement('div');
16 | barBase.style.background = '#aaa';
17 | barBase.style.width = '50%';
18 | barBase.style.minWidth = '250px';
19 | barBase.style.borderRadius = '10px';
20 | barBase.style.height = '15px';
21 | this.domElement.appendChild(barBase);
22 | const bar = document.createElement('div');
23 | bar.style.background = '#22a';
24 | bar.style.width = '50%';
25 | bar.style.borderRadius = '10px';
26 | bar.style.height = '100%';
27 | bar.style.width = '0';
28 | barBase.appendChild(bar);
29 | this.progressBar = bar;
30 |
31 | document.body.appendChild(this.domElement);
32 |
33 | function onprogress(delta) {
34 | const progress = delta * 100;
35 | this.progressBar.style.width = `${progress}%`;
36 | }
37 | }
38 |
39 | set progress(delta) {
40 | const percent = delta * 100;
41 | this.progressBar.style.width = `${percent}%`;
42 | }
43 |
44 | set visible(value) {
45 | if (value) {
46 | this.domElement.style.display = 'flex';
47 | } else {
48 | this.domElement.style.display = 'none';
49 | }
50 | }
51 |
52 | get loaded() {
53 | if (this.assets === undefined) return false;
54 |
55 | let ploaded = 0,
56 | ptotal = 0;
57 | Object.values(this.assets).forEach((asset) => {
58 | ploaded += asset.loaded;
59 | ptotal += asset.total;
60 | });
61 |
62 | return ploaded == ptotal;
63 | }
64 |
65 | update(assetName, loaded, total) {
66 | if (this.assets === undefined) this.assets = {};
67 |
68 | if (this.assets[assetName] === undefined) {
69 | this.assets[assetName] = { loaded, total };
70 | } else {
71 | this.assets[assetName].loaded = loaded;
72 | this.assets[assetName].total = total;
73 | }
74 |
75 | let ploaded = 0,
76 | ptotal = 0;
77 | Object.values(this.assets).forEach((asset) => {
78 | ploaded += asset.loaded;
79 | ptotal += asset.total;
80 | });
81 |
82 | this.progress = ploaded / ptotal;
83 | }
84 | }
85 |
86 | export { LoadingBar };
87 |
--------------------------------------------------------------------------------
/libs/SFX.js:
--------------------------------------------------------------------------------
1 | import { AudioListener, Audio, PositionalAudio, AudioLoader } from '../projects/shootout/three128/three.module.js';
2 |
3 | class SFX{
4 | constructor(camera, assetsPath, listener){
5 | if (listener==null){
6 | this.listener = new AudioListener();
7 | camera.add( this.listener );
8 | }else{
9 | this.listener = listener;
10 | }
11 |
12 | this.assetsPath = assetsPath;
13 |
14 | this.sounds = {};
15 | }
16 |
17 | load(name, loop=false, vol=0.5, obj=null){
18 | // create a global audio source
19 | const sound = (obj==null) ? new Audio( this.listener ) : new PositionalAudio( this.listener );
20 |
21 | this.sounds[name] = sound;
22 |
23 | // load a sound and set it as the Audio object's buffer
24 | const audioLoader = new AudioLoader().setPath(this.assetsPath);
25 | audioLoader.load( `${name}.mp3`, buffer => {
26 | sound.setBuffer( buffer );
27 | sound.setLoop( loop );
28 | sound.setVolume( vol );
29 | });
30 |
31 | if (obj!==null) obj.add(sound);
32 | }
33 |
34 | setVolume(name, volume){
35 | const sound = this.sounds[name];
36 |
37 | if (sound !== undefined) sound.setVolume(volume);
38 | }
39 |
40 | setLoop(name, loop){
41 | const sound = this.sounds[name];
42 |
43 | if (sound !== undefined) sound.setLoop(loop);
44 | }
45 |
46 | play(name){
47 | const sound = this.sounds[name];
48 |
49 | if (sound !== undefined && !sound.isPlaying) sound.play();
50 | }
51 |
52 | stop(name){
53 | const sound = this.sounds[name];
54 |
55 | if (sound !== undefined && sound.isPlaying) sound.stop();
56 | }
57 |
58 | stopAll(){
59 | for(let name in this.sounds) this.stop(name);
60 | }
61 |
62 | pause(name){
63 | const sound = this.sounds[name];
64 |
65 | if (sound !== undefined) sound.pause();
66 | }
67 | }
68 |
69 | export { SFX };
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | import './style.css';
2 | import gsap from 'gsap';
3 | import * as THREE from 'three';
4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
5 | // import * as dat from 'dat.gui';
6 |
7 | // const gui = new dat.GUI();
8 | const world = {
9 | plane: {
10 | width: 800,
11 | height: 800,
12 | widthSegments: 100,
13 | heightSegments: 100,
14 | },
15 | };
16 |
17 | const raycaster = new THREE.Raycaster();
18 | const scene = new THREE.Scene();
19 |
20 | const renderer = new THREE.WebGLRenderer();
21 | renderer.setSize(innerWidth, innerHeight);
22 | renderer.setPixelRatio(devicePixelRatio);
23 |
24 | document.body.append(renderer.domElement);
25 |
26 | /**
27 | * Camera/ Orbital Control
28 | */
29 | const camera = new THREE.PerspectiveCamera(
30 | 75,
31 | innerWidth / innerHeight,
32 | 0.1,
33 | 1000
34 | );
35 | camera.position.y = -75;
36 | camera.position.z = 11;
37 | new OrbitControls(camera, renderer.domElement);
38 |
39 | /**
40 | * Window resize
41 | */
42 | window.addEventListener('resize', () => {
43 | // Update camera
44 | camera.aspect = innerWidth / innerHeight;
45 | camera.updateProjectionMatrix();
46 |
47 | // Update renderer
48 | renderer.setSize(innerWidth, innerHeight);
49 | renderer.setPixelRatio(Math.min(window.devicePixelRatio));
50 | });
51 |
52 | /**
53 | * Plane Shape
54 | */
55 | const planeGeometry = new THREE.PlaneGeometry(
56 | world.plane.width,
57 | world.plane.height,
58 | world.plane.widthSegments,
59 | world.plane.heightSegments
60 | );
61 | const planeMaterial = new THREE.MeshPhongMaterial({
62 | side: THREE.DoubleSide,
63 | flatShading: THREE.FlatShading,
64 | vertexColors: true,
65 | });
66 |
67 | const generatePlaneGeometry = () => {
68 | planeMesh.geometry.dispose();
69 | planeMesh.geometry = new THREE.PlaneGeometry(
70 | world.plane.width,
71 | world.plane.height,
72 | world.plane.widthSegments,
73 | world.plane.heightSegments
74 | );
75 |
76 | // Vertice postion randomization
77 | const { array } = planeMesh.geometry.attributes.position;
78 | const randomValues = [];
79 | for (let i = 0; i < array.length; i += 3) {
80 | const x = array[i];
81 | const y = array[i + 1];
82 | const z = array[i + 2];
83 | array[i] = x + (Math.random() - 0.5) * 3;
84 | array[i + 1] = y + (Math.random() - 0.5) * 3;
85 | array[i + 2] = z + (Math.random() - 0.5) * 3;
86 | randomValues.push(Math.random() * Math.PI * 2);
87 | randomValues.push(Math.random() * Math.PI * 2);
88 | randomValues.push(Math.random() * Math.PI * 2);
89 | }
90 | planeMesh.geometry.attributes.position.originalPosition = array;
91 | planeMesh.geometry.attributes.position.randomValues = randomValues;
92 |
93 | // Color attribute addition
94 | const colors = [];
95 | for (let i = 0; i < planeMesh.geometry.attributes.position.count; i++) {
96 | colors.push(0, 0.19, 0.4);
97 | }
98 | planeMesh.geometry.setAttribute(
99 | 'color',
100 | new THREE.BufferAttribute(new Float32Array(colors), 3)
101 | );
102 | };
103 |
104 | const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
105 | scene.add(planeMesh);
106 | generatePlaneGeometry();
107 |
108 | /**
109 | * Lights
110 | */
111 | const light = new THREE.DirectionalLight(0xffffff, 1.5);
112 | light.position.set(0, 1.6, 1.05);
113 | scene.add(light);
114 | const backLight = new THREE.DirectionalLight(0xffffff, 1);
115 | backLight.position.set(0, 0, -1);
116 | scene.add(backLight);
117 |
118 | /**
119 | * Hover Effect
120 | */
121 | const mouse = { x: undefined, y: undefined };
122 |
123 | addEventListener('mousemove', (e) => {
124 | mouse.x = (e.clientX / innerWidth) * 2 - 1;
125 | mouse.y = (e.clientY / innerHeight) * -2 + 1;
126 | });
127 |
128 | /**
129 | * Animation
130 | */
131 | let frame = 0;
132 | (function animate() {
133 | requestAnimationFrame(animate);
134 | renderer.render(scene, camera);
135 | raycaster.setFromCamera(mouse, camera);
136 | frame += 0.01;
137 | const { array, originalPosition, randomValues } =
138 | planeMesh.geometry.attributes.position;
139 |
140 | for (let i = 0; i < array.length; i += 3) {
141 | array[i] =
142 | originalPosition[i] + Math.cos(frame + randomValues[i]) * 0.02;
143 | array[i + 1] =
144 | originalPosition[i + 1] +
145 | Math.sin(frame + randomValues[i + 1]) * 0.02;
146 | }
147 | planeMesh.geometry.attributes.position.needsUpdate = true;
148 | const intersects = raycaster.intersectObject(planeMesh);
149 | if (intersects.length > 0) {
150 | const { face } = intersects[0];
151 | const { color } = intersects[0].object.geometry.attributes;
152 | // Vertice 1
153 | color.setX(face.a, 0.1);
154 | color.setY(face.a, 0.5);
155 | color.setZ(face.a, 1);
156 | // Vertice 2
157 | color.setX(face.b, 0.1);
158 | color.setY(face.b, 0.5);
159 | color.setZ(face.b, 1);
160 | // Vertice 3
161 | color.setX(face.c, 0.1);
162 | color.setY(face.c, 0.5);
163 | color.setZ(face.c, 1);
164 |
165 | color.needsUpdate = true;
166 |
167 | const initialColor = {
168 | r: 0,
169 | g: 0.19,
170 | b: 0.4,
171 | };
172 | const hoverColor = {
173 | r: 0.1,
174 | g: 0.5,
175 | b: 1,
176 | };
177 |
178 | gsap.to(hoverColor, {
179 | ...initialColor,
180 | onUpdate: () => {
181 | color.setX(face.a, hoverColor.r);
182 | color.setY(face.a, hoverColor.g);
183 | color.setZ(face.a, hoverColor.b);
184 | // Vertice 2
185 | color.setX(face.b, hoverColor.r);
186 | color.setY(face.b, hoverColor.g);
187 | color.setZ(face.b, hoverColor.b);
188 | // Vertice 3
189 | color.setX(face.c, hoverColor.r);
190 | color.setY(face.c, hoverColor.g);
191 | color.setZ(face.c, hoverColor.b);
192 |
193 | color.needsUpdate = true;
194 | },
195 | });
196 | }
197 | })();
198 |
199 | /**
200 | * Development congifuration for DAT.GUI
201 | */
202 | // const planedev = gui.addFolder('Plane');
203 | // planedev.add(world.plane, 'width', 1, 500).onChange(generatePlaneGeometry);
204 | // planedev.add(world.plane, 'height', 1, 500).onChange(generatePlaneGeometry);
205 | // planedev
206 | // .add(world.plane, 'widthSegments', 1, 100)
207 | // .onChange(generatePlaneGeometry);
208 | // planedev
209 | // .add(world.plane, 'heightSegments', 1, 100)
210 | // .onChange(generatePlaneGeometry);
211 |
212 | // const cameraDev = gui.addFolder('Camera');
213 | // cameraDev.add(camera.position, 'x', -100, 100);
214 | // cameraDev.add(camera.position, 'y', -100, 100);
215 | // cameraDev.add(camera.position, 'z', -100, 100);
216 |
217 | // const lightDev = gui.addFolder('Light');
218 | // lightDev.add(light.position, 'x').min(-3).max(3).step(0.01);
219 | // lightDev.add(light.position, 'y').min(-6).max(6).step(0.01);
220 | // lightDev.add(light.position, 'z').min(-3).max(3).step(0.01);
221 | // lightDev.add(light, 'intensity').min(0).max(10).step(0.01);
222 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.0.0",
3 | "scripts": {
4 | "lint": "eslint",
5 | "dev": "vite",
6 | "build": "vite build",
7 | "serve": "vite preview"
8 | },
9 | "devDependencies": {
10 | "dat.gui": "^0.7.7",
11 | "eslint": "^7.27.0",
12 | "vite": "^2.3.5"
13 | },
14 | "dependencies": {
15 | "gsap": "^3.8.0",
16 | "three": "^0.129.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/projects/custom-chairs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 | 3D Model Color Customizer App
11 |
12 |
16 |
17 |
18 |
22 |
23 |
71 |
72 |
73 |
74 |
75 |
78 |
79 | Drag to rotate 360°
82 |
83 |
84 |
85 |
89 |
90 |
91 |
95 |
96 |
97 |
101 |
102 |
103 |
107 |
108 |
109 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | Grab to rotate chair.
122 | Scroll to zoom.
123 | Drag swatches to view more.
124 |
125 |
126 |
127 |
128 |
131 |
132 |
133 |
3D Model Color Customizer App
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/projects/custom-chairs/style.css:
--------------------------------------------------------------------------------
1 | body,
2 | html {
3 | height: 100%;
4 | margin: 0;
5 | padding: 0;
6 | font-family: 'Raleway', sans-serif;
7 | font-size: 14px;
8 | color: #444444;
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | width: 100%;
13 | height: 100%;
14 | position: fixed;
15 | top: 0;
16 | left: 0;
17 | overflow: hidden;
18 | }
19 |
20 | * {
21 | touch-action: manipulation;
22 | }
23 |
24 | *,
25 | *::before,
26 | *::after {
27 | box-sizing: border-box;
28 | }
29 |
30 | .frame {
31 | top: 0;
32 | position: absolute;
33 | left: 0;
34 | padding: 1rem;
35 | }
36 |
37 | .frame__title {
38 | font-size: 1rem;
39 | display: inline-block;
40 | }
41 |
42 | .frame__links {
43 | display: inline-block;
44 | margin: 0 2rem;
45 | text-transform: lowercase;
46 | }
47 |
48 | .frame__links a {
49 | display: inline-block;
50 | margin: 0 0.25rem;
51 | text-decoration: none;
52 | color: red;
53 | }
54 |
55 | .frame__links a:focus,
56 | .frame__links a:hover {
57 | text-decoration: underline;
58 | }
59 |
60 | #c {
61 | width: 100%;
62 | height: 100%;
63 | display: block;
64 | top: 0;
65 | left: 0;
66 | }
67 |
68 | .controls {
69 | position: absolute;
70 | bottom: 0;
71 | width: 100%;
72 | }
73 |
74 | .options {
75 | position: absolute;
76 | left: 0;
77 | }
78 |
79 | .option {
80 | background-size: cover;
81 | background-position: 50%;
82 | background-color: white;
83 | margin-bottom: 3px;
84 | padding: 10px;
85 | height: 55px;
86 | width: 55px;
87 | display: flex;
88 | justify-content: center;
89 | align-items: center;
90 | cursor: pointer;
91 | }
92 |
93 | .option:hover {
94 | border-left: 5px solid white;
95 | width: 58px;
96 | }
97 |
98 | .option.--is-active {
99 | border-right: 3px solid red;
100 | width: 58px;
101 | cursor: default;
102 | }
103 |
104 | .option.--is-active:hover {
105 | border-left: none;
106 | }
107 |
108 | .option img {
109 | height: 100%;
110 | width: auto;
111 | pointer-events: none;
112 | }
113 |
114 | .info {
115 | padding: 0 1em;
116 | display: flex;
117 | justify-content: flex-end;
118 | }
119 |
120 | .info p {
121 | margin-top: 0;
122 | }
123 |
124 | .tray {
125 | width: 100%;
126 | height: 50px;
127 | position: relative;
128 | overflow-x: hidden;
129 | }
130 |
131 | .tray__slide {
132 | position: absolute;
133 | display: flex;
134 | left: 0;
135 | /* transform: translateX(-50%);
136 | animation: wheelin 1s 2s ease-in-out forwards; */
137 | }
138 |
139 | .tray__swatch {
140 | transition: 0.1s ease-in;
141 | height: 50px;
142 | min-width: 50px;
143 | flex: 1;
144 | box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.3);
145 | background-size: cover;
146 | background-position: center;
147 | }
148 |
149 | .tray__swatch:nth-child(5n + 5) {
150 | margin-right: 20px;
151 | }
152 |
153 | .drag-notice {
154 | display: flex;
155 | justify-content: center;
156 | align-items: center;
157 | padding: 2em;
158 | width: 10em;
159 | height: 10em;
160 | box-sizing: border-box;
161 | font-size: 0.9em;
162 | font-weight: 800;
163 | text-transform: uppercase;
164 | text-align: center;
165 | border-radius: 5em;
166 | background: white;
167 | position: absolute;
168 | }
169 |
170 | .drag-notice.start {
171 | -webkit-animation: popout 0.25s 3s forwards;
172 | animation: popout 0.25s 3s forwards;
173 | }
174 |
175 | @-webkit-keyframes popout {
176 | to {
177 | -webkit-transform: scale(0);
178 | transform: scale(0);
179 | }
180 | }
181 |
182 | @keyframes popout {
183 | to {
184 | -webkit-transform: scale(0);
185 | transform: scale(0);
186 | }
187 | }
188 |
189 | @-webkit-keyframes wheelin {
190 | to {
191 | -webkit-transform: translateX(0);
192 | transform: translateX(0);
193 | }
194 | }
195 |
196 | @keyframes wheelin {
197 | to {
198 | -webkit-transform: translateX(0);
199 | transform: translateX(0);
200 | }
201 | }
202 |
203 | @media (max-width: 960px) {
204 | .options {
205 | top: 0;
206 | }
207 |
208 | .info {
209 | padding: 0 1em 1em 0;
210 | }
211 |
212 | .info__message {
213 | display: flex;
214 | align-items: flex-end;
215 | }
216 |
217 | .info__message p {
218 | margin: 0;
219 | font-size: 0.7em;
220 | }
221 |
222 | .frame {
223 | left: auto;
224 | right: 0;
225 | padding-left: 6rem;
226 | }
227 |
228 | .frame__links {
229 | display: block;
230 | margin: 0;
231 | text-align: right;
232 | }
233 | }
234 |
235 | @media (max-width: 720px) {
236 | .info {
237 | flex-direction: column;
238 | justify-content: center;
239 | align-items: center;
240 | padding: 0 1em 1em;
241 | }
242 |
243 | .info__message {
244 | margin-bottom: 1em;
245 | }
246 | }
247 |
248 | @media (max-width: 680px) {
249 | .info {
250 | padding: 1em 2em;
251 | }
252 |
253 | .info__message {
254 | display: none;
255 | }
256 |
257 | .options {
258 | bottom: 50px;
259 | }
260 |
261 | .option {
262 | margin-bottom: 1px;
263 | padding: 5px;
264 | height: 45px;
265 | width: 45px;
266 | display: flex;
267 | }
268 |
269 | .option.--is-active {
270 | border-right: 2px solid red;
271 | width: 47px;
272 | }
273 |
274 | .option img {
275 | height: 100%;
276 | width: auto;
277 | pointer-events: none;
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/projects/plane/Explosion.js:
--------------------------------------------------------------------------------
1 | import {
2 | IcosahedronGeometry,
3 | TextureLoader,
4 | ShaderMaterial,
5 | Mesh,
6 | ShaderChunk,
7 | } from 'three';
8 | import { noise } from '../../libs/Noise.js';
9 | import { Tween } from '../../libs/Toon3D.js';
10 |
11 | class Explosion {
12 | static vshader = `
13 | #include
14 |
15 | uniform float u_time;
16 |
17 | varying float noise;
18 |
19 | void main() {
20 | float time = u_time;
21 | float displacement;
22 | float b;
23 |
24 | // add time to the noise parameters so it's animated
25 | noise = 10.0 * -.10 * turbulence( .5 * normal + time );
26 | b = 5.0 * pnoise( 0.05 * position + vec3( 2.0 * time ), vec3( 100.0 ) );
27 | displacement = - 10. * noise + b;
28 |
29 | // move the position along the normal and transform it
30 | vec3 newPosition = position + normal * displacement;
31 | gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
32 | }
33 | `;
34 | static fshader = `
35 | #define PI 3.141592653589
36 | #define PI2 6.28318530718
37 |
38 | uniform vec2 u_mouse;
39 | uniform vec2 u_resolution;
40 | uniform float u_time;
41 | uniform float u_opacity;
42 | uniform sampler2D u_tex;
43 |
44 | varying float noise;
45 |
46 | //
47 | // By Morgan McGuire @morgan3d, http://graphicscodex.com
48 |
49 | //https://www.clicktorelease.com/blog/vertex-displacement-noise-3d-webgl-glsl-three-js/
50 |
51 | float random( vec3 scale, float seed ){
52 | return fract( sin( dot( gl_FragCoord.xyz + seed, scale ) ) * 43758.5453 + seed ) ;
53 | }
54 |
55 | void main() {
56 |
57 | // get a random offset
58 | float r = .01 * random( vec3( 12.9898, 78.233, 151.7182 ), 0.0 );
59 | // lookup vertically in the texture, using noise and offset
60 | // to get the right RGB colour
61 | vec2 t_pos = vec2( 0, 1.3 * noise + r );
62 | vec4 color = texture2D( u_tex, t_pos );
63 |
64 | gl_FragColor = vec4( color.rgb, u_opacity );
65 | }
66 | `;
67 | constructor(parent, obstacles) {
68 | const geometry = new IcosahedronGeometry(20, 4);
69 |
70 | this.obstacles = obstacles;
71 |
72 | this.uniforms = {
73 | u_time: { value: 0.0 },
74 | u_mouse: { value: { x: 0.0, y: 0.0 } },
75 | u_opacity: { value: 0.6 },
76 | u_resolution: { value: { x: 0, y: 0 } },
77 | u_tex: {
78 | value: new TextureLoader().load(
79 | `/assets/plane/explosion.png`
80 | ),
81 | },
82 | };
83 |
84 | ShaderChunk.noise = noise;
85 |
86 | const material = new ShaderMaterial({
87 | uniforms: this.uniforms,
88 | vertexShader: Explosion.vshader,
89 | fragmentShader: Explosion.fshader,
90 | transparent: true,
91 | opacity: 0.6,
92 | });
93 |
94 | this.ball = new Mesh(geometry, material);
95 | const scale = 0.05;
96 | this.ball.scale.set(scale, scale, scale);
97 | parent.add(this.ball);
98 |
99 | this.tweens = [];
100 | this.tweens.push(
101 | new Tween(
102 | this.ball.scale,
103 | 'x',
104 | 0.2,
105 | 1.5,
106 | this.onComplete.bind(this),
107 | 'outQuad'
108 | )
109 | );
110 |
111 | this.active = true;
112 | }
113 |
114 | onComplete() {
115 | this.ball.parent.remove(this.ball);
116 | this.tweens = [];
117 | this.active = false;
118 | this.ball.geometry.dispose();
119 | this.ball.material.dispose();
120 | if (this.obstacles) this.obstacles.removeExplosion(this);
121 | }
122 |
123 | update(time) {
124 | if (!this.active) return;
125 |
126 | this.uniforms.u_time.value += time;
127 | this.uniforms.u_opacity.value = this.ball.material.opacity;
128 |
129 | if (this.tweens.length < 2) {
130 | const elapsedTime = this.uniforms.u_time.value - 1;
131 |
132 | if (elapsedTime > 0) {
133 | this.tweens.push(
134 | new Tween(this.ball.material, 'opacity', 0, 0.5)
135 | );
136 | }
137 | }
138 |
139 | this.tweens.forEach((tween) => {
140 | tween.update(time);
141 | });
142 |
143 | this.ball.scale.y = this.ball.scale.z = this.ball.scale.x;
144 | }
145 | }
146 |
147 | export { Explosion };
148 |
--------------------------------------------------------------------------------
/projects/plane/Game.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader';
3 | import { LoadingBar } from '@/libs/LoadingBar';
4 | import { Plane } from './Plane.js';
5 | import { Obstacles } from './Obstacles.js';
6 | import { SFX } from './SFX.js';
7 |
8 | class Game {
9 | constructor() {
10 | const container = document.createElement('div');
11 | document.body.appendChild(container);
12 |
13 | this.loadingBar = new LoadingBar();
14 | this.loadingBar.visible = false;
15 |
16 | this.clock = new THREE.Clock();
17 |
18 | this.assetsPath = '../../assets/';
19 |
20 | this.camera = new THREE.PerspectiveCamera(
21 | 70,
22 | window.innerWidth / window.innerHeight,
23 | 0.01,
24 | 100
25 | );
26 | this.camera.position.set(-4.37, 0, -4.75);
27 | this.camera.lookAt(0, 0, 6);
28 |
29 | this.cameraController = new THREE.Object3D();
30 | this.cameraController.add(this.camera);
31 | this.cameraTarget = new THREE.Vector3(0, 0, 6);
32 |
33 | this.scene = new THREE.Scene();
34 | this.scene.add(this.cameraController);
35 |
36 | const ambient = new THREE.HemisphereLight(0xffffff, 0xbbbbff, 1);
37 | ambient.position.set(0.5, 1, 0.25);
38 | this.scene.add(ambient);
39 |
40 | this.renderer = new THREE.WebGLRenderer({
41 | antialias: true,
42 | alpha: true,
43 | });
44 | this.renderer.setPixelRatio(window.devicePixelRatio);
45 | this.renderer.setSize(window.innerWidth, window.innerHeight);
46 | this.renderer.outputEncoding = THREE.sRGBEncoding;
47 | container.appendChild(this.renderer.domElement);
48 | this.setEnvironment();
49 |
50 | this.active = false;
51 | this.load();
52 |
53 | window.addEventListener('resize', this.resize.bind(this));
54 |
55 | document.addEventListener('keydown', this.keyDown.bind(this));
56 | document.addEventListener('keyup', this.keyUp.bind(this));
57 |
58 | document.addEventListener('touchstart', this.mouseDown.bind(this));
59 | document.addEventListener('touchend', this.mouseUp.bind(this));
60 | document.addEventListener('mousedown', this.mouseDown.bind(this));
61 | document.addEventListener('mouseup', this.mouseUp.bind(this));
62 |
63 | this.spaceKey = false;
64 |
65 | const btn = document.getElementById('playBtn');
66 | btn.addEventListener('click', this.startGame.bind(this));
67 | }
68 |
69 | startGame() {
70 | const gameover = document.getElementById('gameover');
71 | const instructions = document.getElementById('instructions');
72 | const btn = document.getElementById('playBtn');
73 |
74 | gameover.style.display = 'none';
75 | instructions.style.display = 'none';
76 | btn.style.display = 'none';
77 |
78 | this.score = 0;
79 | this.bonusScore = 0;
80 | this.lives = 3;
81 |
82 | let elm = document.getElementById('score');
83 | elm.innerHTML = this.score;
84 |
85 | elm = document.getElementById('lives');
86 | elm.innerHTML = this.lives;
87 |
88 | this.plane.reset();
89 | this.obstacles.reset();
90 |
91 | this.active = true;
92 |
93 | this.sfx.play('engine');
94 | }
95 |
96 | resize() {
97 | this.camera.aspect = window.innerWidth / window.innerHeight;
98 | this.camera.updateProjectionMatrix();
99 | this.renderer.setSize(window.innerWidth, window.innerHeight);
100 | }
101 |
102 | keyDown(evt) {
103 | switch (evt.keyCode) {
104 | case 32:
105 | this.spaceKey = true;
106 | break;
107 | }
108 | }
109 |
110 | keyUp(evt) {
111 | switch (evt.keyCode) {
112 | case 32:
113 | this.spaceKey = false;
114 | break;
115 | }
116 | }
117 |
118 | mouseDown(evt) {
119 | this.spaceKey = true;
120 | }
121 |
122 | mouseUp(evt) {
123 | this.spaceKey = false;
124 | }
125 |
126 | setEnvironment() {
127 | const loader = new RGBELoader()
128 | .setDataType(THREE.UnsignedByteType)
129 | .setPath(this.assetsPath);
130 | const pmremGenerator = new THREE.PMREMGenerator(this.renderer);
131 | pmremGenerator.compileEquirectangularShader();
132 |
133 | const self = this;
134 |
135 | loader.load(
136 | 'hdr/venice_sunset_1k.hdr',
137 | (texture) => {
138 | const envMap =
139 | pmremGenerator.fromEquirectangular(texture).texture;
140 | pmremGenerator.dispose();
141 |
142 | self.scene.environment = envMap;
143 | },
144 | undefined,
145 | (err) => {
146 | console.error(err.message);
147 | }
148 | );
149 | }
150 |
151 | load() {
152 | this.loadSkybox();
153 | this.loading = true;
154 | this.loadingBar.visible = true;
155 |
156 | this.plane = new Plane(this);
157 | this.obstacles = new Obstacles(this);
158 |
159 | this.loadSGX();
160 | }
161 |
162 | loadSGX() {
163 | this.sfx = new SFX(this.camera, this.assetsPath + 'plane/');
164 | this.sfx.load('explosion');
165 | this.sfx.load('engine', true, 1);
166 | this.sfx.load('gliss');
167 | this.sfx.load('gameover');
168 | this.sfx.load('bonus');
169 | }
170 |
171 | loadSkybox() {
172 | this.scene.background = new THREE.CubeTextureLoader()
173 | .setPath(`${this.assetsPath}/plane/paintedsky/`)
174 | .load(
175 | ['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg'],
176 | () => {
177 | this.renderer.setAnimationLoop(this.render.bind(this));
178 | }
179 | );
180 | }
181 |
182 | gameOver() {
183 | this.active = false;
184 |
185 | const gameover = document.getElementById('gameover');
186 | const btn = document.getElementById('playBtn');
187 |
188 | gameover.style.display = 'block';
189 | btn.style.display = 'block';
190 |
191 | this.plane.visible = false;
192 |
193 | this.sfx.stopAll();
194 | this.sfx.play('gameover');
195 | }
196 |
197 | incScore() {
198 | this.score++;
199 |
200 | const elm = document.getElementById('score');
201 |
202 | if (this.score % 3 === 0) {
203 | this.bonusScore += 3;
204 | this.sfx.play('bonus');
205 | }
206 |
207 | this.sfx.play('gliss');
208 | elm.innerHTML = this.score + this.bonusScore;
209 | }
210 |
211 | decLives() {
212 | this.lives--;
213 |
214 | const elm = document.getElementById('lives');
215 |
216 | elm.innerHTML = this.lives;
217 |
218 | if (this.lives == 0) setTimeout(this.gameOver.bind(this), 1200);
219 |
220 | this.sfx.play('explosion');
221 | }
222 |
223 | updateCamera() {
224 | this.cameraController.position.copy(this.plane.position);
225 | this.cameraController.position.y = 0;
226 | this.cameraTarget.copy(this.plane.position);
227 | this.cameraTarget.z += 6;
228 | this.camera.lookAt(this.cameraTarget);
229 | }
230 |
231 | render() {
232 | if (this.loading) {
233 | if (this.plane.ready && this.obstacles.ready) {
234 | this.loading = false;
235 | this.loadingBar.visible = false;
236 | } else {
237 | return;
238 | }
239 | }
240 |
241 | const dt = this.clock.getDelta();
242 | const time = this.clock.getElapsedTime();
243 |
244 | this.plane.update(time);
245 |
246 | if (this.active) {
247 | this.obstacles.update(this.plane.position, dt);
248 | }
249 |
250 | this.updateCamera();
251 |
252 | this.renderer.render(this.scene, this.camera);
253 | }
254 | }
255 |
256 | export { Game };
257 |
--------------------------------------------------------------------------------
/projects/plane/Obstacles.js:
--------------------------------------------------------------------------------
1 | import { Group, Vector3 } from 'three';
2 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
3 | import { Explosion } from './Explosion.js';
4 |
5 | class Obstacles {
6 | constructor(game) {
7 | this.assetsPath = game.assetsPath;
8 | this.loadingBar = game.loadingBar;
9 | this.game = game;
10 | this.scene = game.scene;
11 | this.loadStar();
12 | this.loadBomb();
13 | this.tmpPos = new Vector3();
14 | this.explosions = [];
15 | }
16 |
17 | loadStar() {
18 | const loader = new GLTFLoader().setPath(`${this.assetsPath}plane/`);
19 | this.ready = false;
20 |
21 | // Load a glTF resource
22 | loader.load(
23 | // resource URL
24 | 'star.glb',
25 | // called when the resource is loaded
26 | (gltf) => {
27 | this.star = gltf.scene.children[0];
28 |
29 | this.star.name = 'star';
30 |
31 | if (this.bomb !== undefined) this.initialize();
32 | },
33 | // called while loading is progressing
34 | (xhr) => {
35 | this.loadingBar.update('star', xhr.loaded, xhr.total);
36 | },
37 | // called when loading has errors
38 | (err) => {
39 | console.error(err);
40 | }
41 | );
42 | }
43 |
44 | loadBomb() {
45 | const loader = new GLTFLoader().setPath(`${this.assetsPath}plane/`);
46 |
47 | // Load a glTF resource
48 | loader.load(
49 | // resource URL
50 | 'bomb.glb',
51 | // called when the resource is loaded
52 | (gltf) => {
53 | this.bomb = gltf.scene.children[0];
54 |
55 | if (this.star !== undefined) this.initialize();
56 | },
57 | // called while loading is progressing
58 | (xhr) => {
59 | this.loadingBar.update('bomb', xhr.loaded, xhr.total);
60 | },
61 | // called when loading has errors
62 | (err) => {
63 | console.error(err);
64 | }
65 | );
66 | }
67 |
68 | initialize() {
69 | this.obstacles = [];
70 |
71 | const obstacle = new Group();
72 |
73 | obstacle.add(this.star);
74 |
75 | this.bomb.rotation.x = -Math.PI * 0.5;
76 | this.bomb.position.y = 7.5;
77 | obstacle.add(this.bomb);
78 |
79 | let rotate = true;
80 |
81 | for (let y = 5; y > -8; y -= 2.5) {
82 | rotate = !rotate;
83 | if (y == 0) continue;
84 | const bomb = this.bomb.clone();
85 | bomb.rotation.x = rotate ? -Math.PI * 0.5 : 0;
86 | bomb.position.y = y;
87 | obstacle.add(bomb);
88 | }
89 | this.obstacles.push(obstacle);
90 |
91 | this.scene.add(obstacle);
92 |
93 | for (let i = 0; i < 3; i++) {
94 | const obstacle1 = obstacle.clone();
95 |
96 | this.scene.add(obstacle1);
97 | this.obstacles.push(obstacle1);
98 | }
99 |
100 | this.reset();
101 |
102 | this.ready = true;
103 | }
104 |
105 | removeExplosion(explosion) {
106 | const index = this.explosions.indexOf(explosion);
107 | if (index != -1) this.explosions.indexOf(index, 1);
108 | }
109 |
110 | reset() {
111 | this.obstacleSpawn = { pos: 20, offset: 5 };
112 | this.obstacles.forEach((obstacle) => this.respawnObstacle(obstacle));
113 | let count;
114 | while (this.explosions.length > 0 && count < 100) {
115 | this.explosions[0].onComplete();
116 | count++;
117 | }
118 | }
119 |
120 | respawnObstacle(obstacle) {
121 | this.obstacleSpawn.pos += 30;
122 | const offset = (Math.random() * 2 - 1) * this.obstacleSpawn.offset;
123 | this.obstacleSpawn.offset += 0.2;
124 | obstacle.position.set(0, offset, this.obstacleSpawn.pos);
125 | obstacle.children[0].rotation.y = Math.random() * Math.PI * 2;
126 | obstacle.userData.hit = false;
127 | obstacle.children.forEach((child) => {
128 | child.visible = true;
129 | });
130 | }
131 |
132 | update(pos, time) {
133 | let collisionObstacle;
134 |
135 | this.obstacles.forEach((obstacle) => {
136 | obstacle.children[0].rotateY(0.01);
137 | const relativePosZ = obstacle.position.z - pos.z;
138 | if (Math.abs(relativePosZ) < 2) {
139 | collisionObstacle = obstacle;
140 | }
141 | if (relativePosZ < -20) {
142 | this.respawnObstacle(obstacle);
143 | }
144 | });
145 |
146 | if (collisionObstacle !== undefined) {
147 | let minDist = Infinity;
148 | collisionObstacle.children.some((child) => {
149 | child.getWorldPosition(this.tmpPos);
150 | const dist = this.tmpPos.distanceToSquared(pos);
151 | if (dist < minDist) minDist = dist;
152 | if (dist < 5 && !collisionObstacle.userData.hit) {
153 | collisionObstacle.userData.hit = true;
154 | console.log(`Closest obstacle is ${minDist.toFixed(2)}`);
155 | this.hit(child);
156 | return true;
157 | }
158 | });
159 | }
160 |
161 | this.explosions.forEach((explosion) => {
162 | explosion.update(time);
163 | });
164 | }
165 |
166 | hit(obj) {
167 | if (obj.name == 'star') {
168 | obj.visible = false;
169 | this.game.incScore();
170 | } else {
171 | this.explosions.push(new Explosion(obj, this));
172 | this.game.decLives();
173 | }
174 | }
175 | }
176 |
177 | export { Obstacles };
178 |
--------------------------------------------------------------------------------
/projects/plane/Plane.js:
--------------------------------------------------------------------------------
1 | import { Vector3 } from 'three';
2 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
3 |
4 | class Plane {
5 | constructor(game) {
6 | this.assetsPath = game.assetsPath;
7 | this.loadingBar = game.loadingBar;
8 | this.game = game;
9 | this.scene = game.scene;
10 | this.load();
11 | this.tmpPos = new Vector3();
12 | }
13 |
14 | get position() {
15 | if (this.plane !== undefined) this.plane.getWorldPosition(this.tmpPos);
16 | return this.tmpPos;
17 | }
18 |
19 | set visible(mode) {
20 | this.plane.visible = mode;
21 | }
22 |
23 | load() {
24 | const loader = new GLTFLoader().setPath(`${this.assetsPath}plane/`);
25 | this.ready = false;
26 |
27 | // Load a glTF resource
28 | loader.load(
29 | // resource URL
30 | 'microplane.glb',
31 | // called when the resource is loaded
32 | (gltf) => {
33 | this.scene.add(gltf.scene);
34 | this.plane = gltf.scene;
35 | this.velocity = new Vector3(0, 0, 0.1);
36 |
37 | this.propeller = this.plane.getObjectByName('propeller');
38 |
39 | this.ready = true;
40 | },
41 | // called while loading is progressing
42 | (xhr) => {
43 | this.loadingBar.update('plane', xhr.loaded, xhr.total);
44 | },
45 | // called when loading has errors
46 | (err) => {
47 | console.error(err);
48 | }
49 | );
50 | }
51 |
52 | reset() {
53 | this.plane.position.set(0, 0, 0);
54 | this.plane.visible = true;
55 | this.velocity.set(0, 0, 0.1);
56 | }
57 |
58 | update(time) {
59 | if (this.propeller !== undefined) this.propeller.rotateZ(1);
60 |
61 | if (this.game.active) {
62 | if (!this.game.spaceKey) {
63 | this.velocity.y -= 0.001;
64 | } else {
65 | this.velocity.y += 0.001;
66 | }
67 | this.velocity.z += 0.0001;
68 | this.plane.rotation.set(0, 0, Math.sin(time * 3) * 0.2, 'XYZ');
69 | this.plane.translateZ(this.velocity.z);
70 | this.plane.translateY(this.velocity.y);
71 | } else {
72 | this.plane.rotation.set(0, 0, Math.sin(time * 3) * 0.2, 'XYZ');
73 | this.plane.position.y = Math.cos(time) * 1.5;
74 | }
75 | }
76 | }
77 |
78 | export { Plane };
79 |
--------------------------------------------------------------------------------
/projects/plane/SFX.js:
--------------------------------------------------------------------------------
1 | import { AudioListener, Audio, PositionalAudio, AudioLoader } from 'three';
2 |
3 | class SFX {
4 | constructor(camera, assetsPath) {
5 | this.listener = new AudioListener();
6 | camera.add(this.listener);
7 | this.assetsPath = assetsPath;
8 | this.sounds = {};
9 | }
10 |
11 | load(name, loop = false, vol = 0.5, obj = null) {
12 | const sound =
13 | obj === null
14 | ? new Audio(this.listener)
15 | : new PositionalAudio(this.listener);
16 | this.sounds[name] = sound;
17 | const audioLoader = new AudioLoader().setPath(this.assetsPath);
18 | audioLoader.load(`${name}.mp3`, (buffer) => {
19 | sound.setBuffer(buffer);
20 | sound.setLoop(loop);
21 | sound.setVolume(vol);
22 | });
23 | }
24 |
25 | setVolume(name, volume) {
26 | const sound = this.sounds[name];
27 | if (sound) sound.setVolume(volume);
28 | }
29 |
30 | setLoop(name, loop) {
31 | const sound = this.sounds[name];
32 | if (sound) sound.setLoop(loop);
33 | }
34 |
35 | play(name) {
36 | const sound = this.sounds[name];
37 | if (sound && !sound.isPlaying) sound.play();
38 | }
39 |
40 | stop(name) {
41 | const sound = this.sounds[name];
42 | if (sound && sound.isPlaying) sound.stop();
43 | }
44 |
45 | stopAll() {
46 | for (let name in this.sounds) this.stop(name);
47 | }
48 |
49 | pause(name) {
50 | const sound = this.sounds[name];
51 | if (sound && sound.isPlaying) sound.pause();
52 | }
53 | }
54 |
55 | export { SFX };
56 |
--------------------------------------------------------------------------------
/projects/plane/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
93 |
94 | Flappy Plane
95 |
96 |
97 |
98 |
99 | Collect stars. Avoid bombs. Spacebar, mousedown or touch
100 | to climb
101 |
102 |
103 |
104 |
105 |
3
106 |
107 |
108 |
0
109 |
110 |
111 |
112 | Game over
113 | PLAY
114 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/projects/shootout/BulletHandler.js:
--------------------------------------------------------------------------------
1 | import {
2 | Mesh,
3 | CylinderGeometry,
4 | MeshBasicMaterial,
5 | Vector3,
6 | Quaternion
7 | } from 'three';
8 | import { sphereIntersectsCylinder } from './Collisions.js';
9 |
10 | class BulletHandler{
11 | constructor(game){
12 | this.game = game;
13 | this.scene = game.scene;
14 | const geometry = new CylinderGeometry(0.01, 0.01, 0.08);
15 | geometry.rotateX( Math.PI/2 );
16 | geometry.rotateY( Math.PI/2 );
17 | const material = new MeshBasicMaterial();
18 | this.bullet = new Mesh(geometry, material);
19 |
20 | this.bullets = [];
21 |
22 | this.npcs = this.game.npcHandler.npcs;
23 |
24 | this.user = this.game.user;
25 |
26 | this.forward = new Vector3( 0, 0, -1 );
27 | this.xAxis = new Vector3( 1, 0, 0 );
28 | this.tmpVec3 = new Vector3();
29 | this.tmpQuat = new Quaternion();
30 | }
31 |
32 | createBullet( pos, quat, user=false){
33 | const bullet = this.bullet.clone();
34 | bullet.position.copy(pos);
35 | bullet.quaternion.copy(quat);
36 | bullet.userData.targetType = (user) ? 1 : 2;
37 | bullet.userData.distance = 0;
38 | this.scene.add(bullet);
39 | this.bullets.push(bullet);
40 | }
41 |
42 | update(dt){
43 | this.bullets.forEach( bullet => {
44 | let hit = false;
45 | const p1 = bullet.position.clone();
46 | let target;
47 | const dist = dt * 15;
48 | //Move bullet to next position
49 | bullet.translateX(dist);
50 | const p3 = bullet.position.clone();
51 | bullet.position.copy(p1);
52 | const iterations = 1;
53 | const p = this.tmpVec3;
54 |
55 | for(let i=1; i<=iterations; i++){
56 | p.lerpVectors(p1, p3, i/iterations);
57 | if (bullet.userData.targetType==1){
58 | const p2 = this.user.position.clone();
59 | p2.y += 1.2;
60 | hit = sphereIntersectsCylinder(p.x, p.y, p.z, 0.01, p2.x, p2.y, p2.z, 2.4, 0.4);
61 | if (hit) target = this.user;
62 | }else{
63 | this.npcs.some( npc => {
64 | if (!npc.dead){
65 | const p2 = npc.position.clone();
66 | p2.y += 1.5;
67 | hit = sphereIntersectsCylinder(p.x, p.y, p.z, 0.01, p2.x, p2.y, p2.z, 3.0, 0.5);
68 | if (hit){
69 | target = npc;
70 | return true;
71 | }
72 | }
73 | })
74 | }
75 | if (hit) break;
76 | }
77 |
78 | if (hit){
79 | target.action = 'shot';
80 | bullet.userData.remove = true;
81 | }else{
82 | bullet.translateX(dist);
83 | bullet.rotateX(dt * 0.3);
84 | bullet.userData.distance += dist;
85 | bullet.userData.remove = (bullet.userData.distance > 50);
86 | }
87 | });
88 |
89 | let found = false;
90 | do{
91 | let remove;
92 | found = this.bullets.some( bullet => {
93 | if (bullet.userData.remove){
94 | remove = bullet;
95 | return true;
96 | }
97 | });
98 | if (found){
99 | const index = this.bullets.indexOf(remove);
100 | if (index!==-1) this.bullets.splice(index, 1);
101 | this.scene.remove(remove);
102 | }
103 |
104 | }while(found);
105 | }
106 | }
107 |
108 | export { BulletHandler };
--------------------------------------------------------------------------------
/projects/shootout/Collisions.js:
--------------------------------------------------------------------------------
1 | //Adapted from https://github.com/bytezeroseven/AA.js
2 |
3 | function boxIntersectsBox(
4 | aminx,
5 | aminy,
6 | aminz,
7 | amaxx,
8 | amaxy,
9 | amaxz,
10 | bminx,
11 | bminy,
12 | bminz,
13 | bmaxx,
14 | bmaxy,
15 | bmaxz
16 | ) {
17 | var overlapX = Math.min(amaxx, bmaxx) - Math.max(aminx, bminx);
18 |
19 | if (overlapX < 0) return false;
20 |
21 | var overlapY = Math.min(amaxy, bmaxy) - Math.max(aminy, bminy);
22 |
23 | if (overlapY < 0) return false;
24 |
25 | var overlapZ = Math.min(amaxz, bmaxz) - Math.max(aminz, bminz);
26 |
27 | if (overlapZ < 0) return false;
28 |
29 | var minOverlap = Math.min(overlapX, overlapY, overlapZ);
30 | var x = (y = z = 0);
31 |
32 | switch (minOverlap) {
33 | case overlapX:
34 | x = Math.sign(aminx + amaxx - bminx - bmaxx);
35 | break;
36 |
37 | case overlapY:
38 | y = Math.sign(aminy + amaxy - bminy - bmaxy);
39 | break;
40 |
41 | case overlapZ:
42 | z = Math.sign(aminz + amaxz - bminz - bmaxz);
43 | break;
44 | }
45 |
46 | return {
47 | minOverlap: minOverlap,
48 | mtvX: x,
49 | mtvY: y,
50 | mtvZ: z,
51 | };
52 | }
53 |
54 | function boxIntersectsSphere(
55 | minx,
56 | miny,
57 | minz,
58 | maxx,
59 | maxy,
60 | maxz,
61 | sx,
62 | sy,
63 | sz,
64 | sr
65 | ) {
66 | var x = Math.max(minx, Math.min(maxx, sx)) - sx;
67 | var y = Math.max(miny, Math.min(maxy, sy)) - sy;
68 | var z = Math.max(minz, Math.min(maxz, sz)) - sz;
69 |
70 | var distance = Math.hypot(x, y, z);
71 |
72 | if (distance > sr) return false;
73 |
74 | var overlap = sr - distance;
75 |
76 | if (distance > 0) {
77 | x /= distance;
78 | y /= distance;
79 | z /= distance;
80 | } else {
81 | x = 1;
82 | y = 0;
83 | z = 0;
84 | }
85 |
86 | return {
87 | minOverlap: overlap,
88 | mtvX: x,
89 | mtvY: y,
90 | mtvZ: z,
91 | };
92 | }
93 |
94 | function boxIntersectsCylinder(
95 | minx,
96 | miny,
97 | minz,
98 | maxx,
99 | maxy,
100 | maxz,
101 | cx,
102 | cy,
103 | cz,
104 | ch,
105 | cr
106 | ) {
107 | var ch2 = ch / 2;
108 |
109 | var overlapY = Math.min(maxy, cy + ch2) - Math.max(miny, cy - ch2);
110 |
111 | if (overlapY < 0) return false;
112 |
113 | var x = Math.max(minx, Math.min(maxx, cx)) - cx;
114 | var z = Math.max(minz, Math.min(maxz, cz)) - cz;
115 |
116 | var distance = Math.hypot(x, z);
117 |
118 | if (distance > cr) return false;
119 |
120 | var overlapXZ = cr - distance;
121 |
122 | var minOverlap, y;
123 |
124 | if (overlapY < overlapXZ) {
125 | minOverlap = overlapY;
126 |
127 | y = Math.sign((miny + maxy) / 2 - cy);
128 | x = 0;
129 | z = 0;
130 | } else {
131 | minOverlap = overlapXZ;
132 |
133 | y = 0;
134 |
135 | if (distance > 0) {
136 | x /= distance;
137 | z /= distance;
138 | } else {
139 | x = 1;
140 | z = 0;
141 | }
142 | }
143 |
144 | return {
145 | minOverlap: minOverlap,
146 | mtvX: x,
147 | mtvY: y,
148 | mtvZ: z,
149 | };
150 | }
151 |
152 | function sphereIntersectsSphere(ax, ay, az, ar, bx, by, bz, br) {
153 | var x = ax - bx;
154 | var y = ay - by;
155 | var z = az - bz;
156 |
157 | var distance = Math.hypot(x, y, z);
158 |
159 | if (distance > ar + br) return false;
160 |
161 | var overlap = ar + br - distance;
162 |
163 | if (distance > 0) {
164 | x /= distance;
165 | y /= distance;
166 | z /= distance;
167 | } else {
168 | x = 1;
169 | y = 0;
170 | z = 0;
171 | }
172 |
173 | return {
174 | minOverlap: overlap,
175 | mtvX: x,
176 | mtvY: y,
177 | mtvZ: z,
178 | };
179 | }
180 |
181 | function sphereIntersectsCylinder(sx, sy, sz, sr, cx, cy, cz, ch, cr) {
182 | var ch2 = ch / 2;
183 |
184 | var overlapY = Math.min(sy + sr, cy + ch2) - Math.max(sy - sr, cy - ch2);
185 |
186 | if (overlapY < 0) return false;
187 |
188 | var newRadius;
189 |
190 | var h1, h2;
191 |
192 | if (overlapY < sr) {
193 | h1 = sr - overlapY;
194 |
195 | newRadius = Math.sqrt(sr * sr - h1 * h1);
196 | } else {
197 | newRadius = sr;
198 | }
199 |
200 | var x = sx - cx;
201 | var z = sz - cz;
202 |
203 | var distance = Math.hypot(x, z);
204 | if (distance > newRadius + cr) return false;
205 |
206 | var overlapXZ = newRadius + cr - distance;
207 |
208 | var minOverlap, y;
209 |
210 | if (overlapY < overlapXZ) {
211 | minOverlap = overlapY;
212 | y = Math.sign(sy - cy);
213 | x = 0;
214 | z = 0;
215 | } else if (overlapY < sr) {
216 | var newerRadius = newRadius - overlapXZ;
217 |
218 | h2 = Math.sqrt(sr * sr - newerRadius * newerRadius);
219 | minOverlap = h2 - h1;
220 |
221 | y = Math.sign(sy - cy);
222 | x = 0;
223 | z = 0;
224 | } else {
225 | minOverlap = overlapXZ;
226 |
227 | x /= distance;
228 | z /= distance;
229 | y = 0;
230 | }
231 |
232 | return {
233 | minOverlap: minOverlap,
234 | mtvX: x,
235 | mtvY: y,
236 | mtvZ: z,
237 | };
238 | }
239 |
240 | function cylinderIntersectsCylinder(ax, ay, az, ah, ar, bx, by, bz, bh, br) {
241 | var ah2 = ah / 2;
242 | var bh2 = bh / 2;
243 |
244 | var overlapY = Math.min(ay + ah2, by + bh2) - Math.max(ay - ah2, by - bh2);
245 |
246 | if (overlapY < 0) return false;
247 |
248 | var x = ax - bx;
249 | var z = az - bz;
250 |
251 | var distance = Math.hypot(x, y);
252 |
253 | if (distance > ar + br) return false;
254 |
255 | var overlapXZ = ar + br - distance;
256 |
257 | var minOverlap, y;
258 |
259 | if (overlapY < overlapXZ) {
260 | minOverlap = overlapY;
261 |
262 | y = Math.sign(sy - cy);
263 | x = 0;
264 | z = 0;
265 | } else {
266 | minOverlap = overlapXZ;
267 |
268 | x /= distance;
269 | z /= distance;
270 | y = 0;
271 | }
272 |
273 | return {
274 | minOverlap: minOverlap,
275 | mtvX: x,
276 | mtvY: y,
277 | mtvZ: z,
278 | };
279 | }
280 |
281 | function sphereIntersectsBox(
282 | sx,
283 | sy,
284 | sz,
285 | sr,
286 | minx,
287 | miny,
288 | minz,
289 | maxx,
290 | maxy,
291 | maxz
292 | ) {
293 | var result = boxIntersectsSphere(
294 | minx,
295 | miny,
296 | minz,
297 | maxx,
298 | maxy,
299 | maxz,
300 | sx,
301 | sy,
302 | sz,
303 | sr
304 | );
305 |
306 | if (result) {
307 | result.mtvX *= -1;
308 | result.mtvY *= -1;
309 | result.mtvZ *= -1;
310 | }
311 |
312 | return result;
313 | }
314 |
315 | function cylinderIntersectsSphere(cx, cy, cz, ch, cr, sx, sy, sz, sr) {
316 | var result = sphereIntersectsCylinder(sx, sy, sz, sr, cx, cy, cz, ch, cr);
317 |
318 | if (result) {
319 | result.mtvX *= -1;
320 | result.mtvY *= -1;
321 | result.mtvZ *= -1;
322 | }
323 |
324 | return result;
325 | }
326 |
327 | function cylinderIntersectsBox(
328 | cx,
329 | cy,
330 | cz,
331 | ch,
332 | cr,
333 | minx,
334 | miny,
335 | minz,
336 | maxx,
337 | maxy,
338 | maxz
339 | ) {
340 | var result = boxIntersectsCylinder(
341 | minx,
342 | miny,
343 | minz,
344 | maxx,
345 | maxy,
346 | maxz,
347 | cx,
348 | cy,
349 | cz,
350 | ch,
351 | cr
352 | );
353 |
354 | if (result) {
355 | result.mtvX *= -1;
356 | result.mtvY *= -1;
357 | result.mtvZ *= -1;
358 | }
359 |
360 | return result;
361 | }
362 |
363 | export {
364 | boxIntersectsBox,
365 | boxIntersectsSphere,
366 | boxIntersectsCylinder,
367 | sphereIntersectsSphere,
368 | sphereIntersectsBox,
369 | sphereIntersectsCylinder,
370 | cylinderIntersectsBox,
371 | cylinderIntersectsSphere,
372 | cylinderIntersectsCylinder,
373 | };
374 |
--------------------------------------------------------------------------------
/projects/shootout/Controller.js:
--------------------------------------------------------------------------------
1 | import { Object3D, Camera, Vector3, Quaternion, Raycaster } from './three128/three.module.js';
2 | import { JoyStick } from '@/libs/JoyStick.js';
3 | //import { Game } from './Game.js';
4 |
5 | class Controller{
6 | constructor(game){
7 | this.camera = game.camera;
8 | this.clock = game.clock;
9 | this.user = game.user;
10 | this.target = game.user.root;
11 | this.navmesh = game.navmesh;
12 | this.game = game;
13 |
14 | this.raycaster = new Raycaster();
15 |
16 | this.move = { up:0, right:0 };
17 | this.look = { up:0, right:0 };
18 |
19 | this.tmpVec3 = new Vector3();
20 | this.tmpQuat = new Quaternion();
21 |
22 | //Used to return the camera to its base position and orientation after a look event
23 | this.cameraBase = new Object3D();
24 | this.cameraBase.position.copy( this.camera.position );
25 | this.cameraBase.quaternion.copy( this.camera.quaternion );
26 | this.target.attach( this.cameraBase );
27 | this.target.rotateY(0.7);
28 |
29 | this.cameraHigh = new Camera();
30 | this.cameraHigh.position.copy( this.camera.position );
31 | this.cameraHigh.position.y += 10;
32 | this.cameraHigh.lookAt( this.target.position );
33 | this.target.attach( this.cameraHigh );
34 |
35 | this.yAxis = new Vector3(0, 1, 0);
36 | this.xAxis = new Vector3(1, 0, 0);
37 | this.forward = new Vector3(0, 0, 1);
38 | this.down = new Vector3(0, -1, 0);
39 |
40 | this.speed = 5;
41 |
42 | this.checkForGamepad();
43 |
44 | if('ontouchstart' in document.documentElement){
45 | const options1 = {
46 | left: true,
47 | app: this,
48 | onMove: this.onMove
49 | }
50 |
51 | const joystick1 = new JoyStick(options1);
52 |
53 | const options2 = {
54 | right: true,
55 | app: this,
56 | onMove: this.onLook
57 | }
58 |
59 | const joystick2 = new JoyStick(options2);
60 |
61 | const fireBtn = document.createElement("div");
62 | fireBtn.style.cssText = "position:absolute; bottom:55px; width:40px; height:40px; background:#FFFFFF; border:#444 solid medium; border-radius:50%; left:50%; transform:translateX(-50%);";
63 | fireBtn.addEventListener('mousedown', this.fire.bind(this, true));
64 | fireBtn.addEventListener('mouseup', this.fire.bind(this, false));
65 | document.body.appendChild(fireBtn);
66 |
67 | this.touchController = { joystick1, joystick2, fireBtn };
68 | }else{
69 | document.addEventListener('keydown', this.keyDown.bind(this));
70 | document.addEventListener('keyup', this.keyUp.bind(this));
71 | document.addEventListener('mousedown', this.mouseDown.bind(this));
72 | document.addEventListener('mouseup', this.mouseUp.bind(this));
73 | document.addEventListener('mousemove', this.mouseMove.bind(this));
74 | this.keys = {
75 | w:false,
76 | a:false,
77 | d:false,
78 | s:false,
79 | space:false,
80 | mousedown:false,
81 | mouseorigin:{x:0, y:0}
82 | };
83 | }
84 | }
85 |
86 | checkForGamepad(){
87 | const gamepads = {};
88 |
89 | const self = this;
90 |
91 | function gamepadHandler(event, connecting) {
92 | const gamepad = event.gamepad;
93 |
94 | if (connecting) {
95 | gamepads[gamepad.index] = gamepad;
96 | self.gamepad = gamepad;
97 | if (self.touchController) self.showTouchController(false);
98 | } else {
99 | delete self.gamepad;
100 | delete gamepads[gamepad.index];
101 | if (self.touchController) self.showTouchController(true);
102 | }
103 | }
104 |
105 | window.addEventListener("gamepadconnected", function(e) { gamepadHandler(e, true); }, false);
106 | window.addEventListener("gamepaddisconnected", function(e) { gamepadHandler(e, false); }, false);
107 | }
108 |
109 | showTouchController(mode){
110 | if (this.touchController == undefined) return;
111 |
112 | this.touchController.joystick1.visible = mode;
113 | this.touchController.joystick2.visible = mode;
114 | this.touchController.fireBtn.style.display = mode ? 'block' : 'none';
115 | }
116 |
117 | keyDown(e){
118 | //console.log('keyCode:' + e.keyCode);
119 | let repeat = false;
120 | if (e.repeat !== undefined) {
121 | repeat = e.repeat;
122 | }
123 | switch(e.keyCode){
124 | case 87:
125 | this.keys.w = true;
126 | break;
127 | case 65:
128 | this.keys.a = true;
129 | break;
130 | case 83:
131 | this.keys.s = true;
132 | break;
133 | case 68:
134 | this.keys.d = true;
135 | break;
136 | case 32:
137 | if (!repeat) this.fire(true);
138 | break;
139 | }
140 | }
141 |
142 | keyUp(e){
143 | switch(e.keyCode){
144 | case 87:
145 | this.keys.w = false;
146 | if (!this.keys.s) this.move.up = 0;
147 | break;
148 | case 65:
149 | this.keys.a = false;
150 | if (!this.keys.d) this.move.right = 0;
151 | break;
152 | case 83:
153 | this.keys.s = false;
154 | if (!this.keys.w) this.move.up = 0;
155 | break;
156 | case 68:
157 | this.keys.d = false;
158 | if (!this.keys.a) this.move.right = 0;
159 | break;
160 | case 32:
161 | this.fire(false);
162 | break;
163 | }
164 | }
165 |
166 | mouseDown(e){
167 | this.keys.mousedown = true;
168 | this.keys.mouseorigin.x = e.offsetX;
169 | this.keys.mouseorigin.y = e.offsetY;
170 | }
171 |
172 | mouseUp(e){
173 | this.keys.mousedown = false;
174 | this.look.up = 0;
175 | this.look.right = 0;
176 | }
177 |
178 | mouseMove(e){
179 | if (!this.keys.mousedown) return;
180 | let offsetX = e.offsetX - this.keys.mouseorigin.x;
181 | let offsetY = e.offsetY - this.keys.mouseorigin.y;
182 | if (offsetX<-100) offsetX = -100;
183 | if (offsetX>100) offsetX = 100;
184 | offsetX /= 100;
185 | if (offsetY<-100) offsetY = -100;
186 | if (offsetY>100) offsetY = 100;
187 | offsetY /= 100;
188 | this.onLook(-offsetY, offsetX);
189 | }
190 |
191 | fire(mode){
192 | //console.log(`Fire:${mode}`);
193 | if (this.game.active) this.user.firing = mode;
194 | }
195 |
196 | onMove( up, right ){
197 | this.move.up = up;
198 | this.move.right = -right;
199 | }
200 |
201 | onLook( up, right ){
202 | this.look.up = up*0.25;
203 | this.look.right = -right;
204 | }
205 |
206 | gamepadHandler(){
207 | const gamepads = navigator.getGamepads();
208 | const gamepad = gamepads[this.gamepad.index];
209 | const leftStickX = gamepad.axes[0];
210 | const leftStickY = gamepad.axes[1];
211 | const rightStickX = gamepad.axes[2];
212 | const rightStickY = gamepad.axes[3];
213 | const fire = gamepad.buttons[7].pressed;
214 | this.onMove(-leftStickY, leftStickX);
215 | this.onLook(-rightStickY, rightStickX);
216 | this.fire(fire);
217 | }
218 |
219 | keyHandler(){
220 | if (this.keys.w) this.move.up += 0.1;
221 | if (this.keys.s) this.move.up -= 0.1;
222 | if (this.keys.a) this.move.right += 0.1;
223 | if (this.keys.d) this.move.right -= 0.1;
224 | if (this.move.up>1) this.move.up = 1;
225 | if (this.move.up<-1) this.move.up = -1;
226 | if (this.move.right>1) this.move.right = 1;
227 | if (this.move.right<-1) this.move.right = -1;
228 | }
229 |
230 | update(dt=0.0167){
231 | if (!this.game.active){
232 | let lerpSpeed = 0.03;
233 | this.cameraBase.getWorldPosition(this.tmpVec3);
234 | this.game.seeUser(this.tmpVec3, true);
235 | this.cameraBase.getWorldQuaternion(this.tmpQuat);
236 | this.camera.position.lerp(this.tmpVec3, lerpSpeed);
237 | this.camera.quaternion.slerp(this.tmpQuat, lerpSpeed);
238 | return;
239 | }
240 |
241 | let playerMoved = false;
242 | let speed;
243 |
244 | if (this.gamepad){
245 | this.gamepadHandler();
246 | }else if(this.keys){
247 | this.keyHandler();
248 | }
249 |
250 | if (this.move.up!=0){
251 | const forward = this.forward.clone().applyQuaternion(this.target.quaternion);
252 | speed = this.move.up>0 ? this.speed * dt : this.speed * dt * 0.3;
253 | speed *= this.move.up;
254 | if (this.user.isFiring && speed>0.03) speed = 0.02;
255 | const pos = this.target.position.clone().add(forward.multiplyScalar(speed));
256 | pos.y += 2;
257 | //console.log(`Moving>> target rotation:${this.target.rotation} forward:${forward} pos:${pos}`);
258 |
259 | this.raycaster.set( pos, this.down );
260 |
261 | const intersects = this.raycaster.intersectObject( this.navmesh );
262 |
263 | if ( intersects.length>0 ){
264 | this.target.position.copy(intersects[0].point);
265 | playerMoved = true;
266 | }
267 | }else{
268 | speed = 0;
269 | }
270 |
271 | this.user.speed = speed;
272 |
273 | if (Math.abs(this.move.right)>0.1){
274 | const theta = dt * (this.move.right-0.1) * 1;
275 | this.target.rotateY(theta);
276 | playerMoved = true;
277 | }
278 |
279 | if (playerMoved){
280 | //this.cameraBase.getWorldPosition(this.tmpVec3);
281 | //this.camera.position.lerp(this.tmpVec3, 0.7);
282 | //if (speed) console.log(speed.toFixed(2));
283 | let run = false;
284 | if (speed>0.03){
285 | if (this.overRunSpeedTime){
286 | const elapsedTime = this.clock.elapsedTime - this.overRunSpeedTime;
287 | run = elapsedTime>0.1;
288 | }else{
289 | this.overRunSpeedTime = this.clock.elapsedTime;
290 | }
291 | }else{
292 | delete this.overRunSpeedTime;
293 | }
294 | if (run){
295 | this.user.action = 'run';
296 | }else{
297 | this.user.action = (this.user.isFiring) ? 'firingwalk' : 'walk';
298 | }
299 | }else{
300 | if (this.user !== undefined && !this.user.isFiring) this.user.action = 'idle';
301 | }
302 |
303 | if (this.look.up==0 && this.look.right==0){
304 | let lerpSpeed = 0.7;
305 | this.cameraBase.getWorldPosition(this.tmpVec3);
306 | if (this.game.seeUser(this.tmpVec3, true)){
307 | this.cameraBase.getWorldQuaternion(this.tmpQuat);
308 | }else{
309 | this.cameraHigh.getWorldPosition(this.tmpVec3);
310 | this.cameraHigh.getWorldQuaternion(this.tmpQuat);
311 | }
312 | this.camera.position.lerp(this.tmpVec3, lerpSpeed);
313 | this.camera.quaternion.slerp(this.tmpQuat, lerpSpeed);
314 | }else{
315 | const delta = 1 * dt;
316 | this.camera.rotateOnWorldAxis(this.yAxis, this.look.right * delta);
317 | const cameraXAxis = this.xAxis.clone().applyQuaternion(this.camera.quaternion);
318 | this.camera.rotateOnWorldAxis(cameraXAxis, this.look.up * delta);
319 | }
320 | }
321 | }
322 |
323 | export { Controller };
--------------------------------------------------------------------------------
/projects/shootout/NPC.js:
--------------------------------------------------------------------------------
1 | import * as THREE from './three128/three.module.js';
2 | import { SFX } from '@/libs/SFX.js';
3 |
4 | class NPC{
5 | constructor(options){
6 | const fps = options.fps || 30; //default fps
7 |
8 | this.name = options.name | 'NPC';
9 |
10 | this.animations = {};
11 |
12 | options.app.scene.add(options.object);
13 |
14 | this.object = options.object;
15 | this.pathLines = new THREE.Object3D();
16 | this.pathColor = new THREE.Color(0xFFFFFF);
17 | options.app.scene.add(this.pathLines);
18 |
19 | this.showPath = options.showPath | false;
20 |
21 | this.waypoints = options.waypoints;
22 |
23 | this.dead = false;
24 |
25 | this.speed = options.speed;
26 | this.app = options.app;
27 |
28 | if (options.app.pathfinder){
29 | this.pathfinder = options.app.pathfinder;
30 | this.ZONE = options.zone;
31 | this.navMeshGroup = this.pathfinder.getGroup(this.ZONE, this.object.position);
32 | }
33 |
34 | const pt = this.object.position.clone();
35 | pt.z += 10;
36 | this.object.lookAt(pt);
37 |
38 | this.rifle = options.rifle;
39 | this.aim = options.aim;
40 | this.enemy = this.app.user.root;
41 | this.isFiring = false;
42 | this.raycaster = new THREE.Raycaster();
43 | this.forward = new THREE.Vector3(0, 0, 1);
44 | this.tmpVec = new THREE.Vector3();
45 | this.tmpQuat = new THREE.Quaternion();
46 | this.aggro = false;
47 |
48 | if (options.animations){
49 | this.mixer = new THREE.AnimationMixer(options.object);
50 | options.animations.forEach( (animation) => {
51 | this.animations[animation.name.toLowerCase()] = animation;
52 | })
53 | }
54 |
55 | this.initRifleDirection();
56 | }
57 |
58 | initRifleDirection(){
59 | this.rifleDirection = {};
60 |
61 | this.rifleDirection.idle = new THREE.Quaternion(-0.044, -0.061, 0.865, 0.495);
62 | this.rifleDirection.firing = new THREE.Quaternion(-0.147, -0.040, 0.784, 0.600);
63 | this.rifleDirection.walking = new THREE.Quaternion( 0.046, -0.017, 0.699, 0.712);
64 | this.rifleDirection.shot = new THREE.Quaternion(-0.133, -0.144, -0.635, 0.747);
65 | }
66 |
67 | initSounds(){
68 | const assetsPath = `${this.app.assetsPath}factory/sfx/`;
69 | this.sfx = new SFX(this.app.camera, assetsPath, this.app.listener);
70 | this.sfx.load('footsteps', true, 0.6, this.object);
71 | this.sfx.load('groan', false, 0.6, this.object);
72 | this.sfx.load('shot', false, 0.6, this.object);
73 | }
74 |
75 | reset(){
76 | this.dead = false;
77 | this.object.position.copy(this.randomWaypoint);
78 | let pt = this.randomWaypoint;
79 | let count = 0;
80 | while(this.object.position.distanceToSquared(pt)<1 && count<10){
81 | pt = this.randomWaypoint;
82 | count++;
83 | }
84 | this.newPath(pt);
85 | }
86 |
87 | get randomWaypoint(){
88 | const index = Math.floor(Math.random()*this.waypoints.length);
89 | return this.waypoints[index];
90 | }
91 |
92 | setTargetDirection(pt){
93 | const player = this.object;
94 | pt.y = player.position.y;
95 | const quaternion = player.quaternion.clone();
96 | player.lookAt(pt);
97 | this.quaternion = player.quaternion.clone();
98 | player.quaternion.copy(quaternion);
99 | }
100 |
101 | newPath(pt){
102 | const player = this.object;
103 |
104 | if (this.pathfinder===undefined){
105 | this.calculatedPath = [ pt.clone() ];
106 | //Calculate target direction
107 | this.setTargetDirection( pt.clone() );
108 | this.action = 'walking';
109 | return;
110 | }
111 |
112 | if (this.sfx) this.sfx.play('footsteps');
113 | //console.log(`New path to ${pt.x.toFixed(1)}, ${pt.y.toFixed(2)}, ${pt.z.toFixed(2)}`);
114 |
115 | const targetGroup = this.pathfinder.getGroup(this.ZONE, pt);
116 | const closestTargetNode = this.pathfinder.getClosestNode(pt, this.ZONE, targetGroup);
117 |
118 | // Calculate a path to the target and store it
119 | this.calculatedPath = this.pathfinder.findPath(player.position, pt, this.ZONE, this.navMeshGroup);
120 |
121 | if (this.calculatedPath && this.calculatedPath.length) {
122 | this.action = 'walking';
123 |
124 | this.setTargetDirection( this.calculatedPath[0].clone() );
125 |
126 | if (this.showPath){
127 | if (this.pathLines) this.app.scene.remove(this.pathLines);
128 |
129 | const material = new THREE.LineBasicMaterial({
130 | color: this.pathColor,
131 | linewidth: 2
132 | });
133 |
134 | const points = [player.position];
135 |
136 | // Draw debug lines
137 | this.calculatedPath.forEach( function(vertex){
138 | points.push(vertex.clone());
139 | });
140 |
141 | let geometry = new THREE.BufferGeometry().setFromPoints( points );
142 |
143 | this.pathLines = new THREE.Line( geometry, material );
144 | this.app.scene.add( this.pathLines );
145 |
146 | // Draw debug spheres except the last one. Also, add the player position.
147 | const debugPath = [player.position].concat(this.calculatedPath);
148 |
149 | debugPath.forEach(vertex => {
150 | geometry = new THREE.SphereGeometry( 0.2 );
151 | const material = new THREE.MeshBasicMaterial( {color: this.pathColor} );
152 | const node = new THREE.Mesh( geometry, material );
153 | node.position.copy(vertex);
154 | this.pathLines.add( node );
155 | });
156 | }
157 | } else {
158 | if (this.sfx) this.sfx.stop('footsteps');
159 |
160 | this.action = 'idle';
161 |
162 | if (this.pathfinder){
163 | const closestPlayerNode = this.pathfinder.getClosestNode(player.position, this.ZONE, this.navMeshGroup);
164 | const clamped = new THREE.Vector3();
165 | this.pathfinder.clampStep(
166 | player.position,
167 | pt.clone(),
168 | closestPlayerNode,
169 | this.ZONE,
170 | this.navMeshGroup,
171 | clamped);
172 | }
173 |
174 | if (this.pathLines) this.app.scene.remove(this.pathLines);
175 | }
176 | }
177 |
178 | set action(name){
179 | if (this.actionName == name.toLowerCase()) return;
180 |
181 | const clip = this.animations[name.toLowerCase()];
182 |
183 | if (clip!==undefined){
184 | const action = this.mixer.clipAction( clip );
185 | if (name=='shot'){
186 | action.clampWhenFinished = true;
187 | action.setLoop( THREE.LoopOnce );
188 | this.dead = true;
189 | if (this.sfx){
190 | this.sfx.stop('footsteps');
191 | this.sfx.play('groan');
192 | }
193 | delete this.calculatedPath;
194 | }
195 | action.reset();
196 | const nofade = this.actionName == 'shot';
197 | this.actionName = name.toLowerCase();
198 | action.play();
199 | if (this.curAction){
200 | if (nofade){
201 | this.curAction.enabled = false;
202 | }else{
203 | this.curAction.crossFadeTo(action, 0.5);
204 | }
205 | }
206 | this.curAction = action;
207 | }
208 | if (this.rifle && this.rifleDirection){
209 | const q = this.rifleDirection[name.toLowerCase()];
210 | if (q!==undefined){
211 | const start = new THREE.Quaternion();
212 | start.copy(this.rifle.quaternion);
213 | this.rifle.quaternion.copy(q);
214 | this.rifle.rotateX(1.57);
215 | const end = new THREE.Quaternion();
216 | end.copy(this.rifle.quaternion);
217 | this.rotateRifle = { start, end, time:0 };
218 | this.rifle.quaternion.copy( start );
219 | }
220 | }
221 | }
222 |
223 | get position(){
224 | return this.object.position;
225 | }
226 |
227 | withinAggroRange(){
228 | const distSq = this.object.position.distanceToSquared(this.enemy.position);
229 | return distSq < 400;
230 | }
231 |
232 | withinFOV( fov ){
233 | const rads = fov / 360 * Math.PI;
234 | const v1 = this.forward.clone().applyQuaternion( this.object.quaternion );
235 | const v2 = this.enemy.position.clone().sub(this.object.position).normalize();
236 | const theta = Math.abs(v1.angleTo(v2)) ;
237 | return theta < rads;
238 | }
239 |
240 | set firing(mode){
241 | this.isFiring = mode;
242 | if (mode){
243 | this.action = "firingwalk";
244 | this.bulletTime = this.app.clock.getElapsedTime();
245 | }else{
246 | this.newPath(this.randomWaypoint);
247 | }
248 | }
249 |
250 | shoot(){
251 | if (this.bulletHandler === undefined) this.bulletHandler = this.app.bulletHandler;
252 | this.aim.getWorldPosition(this.tmpVec);
253 | this.aim.getWorldQuaternion(this.tmpQuat);
254 | this.bulletHandler.createBullet( this.tmpVec, this.tmpQuat, true );
255 | this.bulletTime = this.app.clock.getElapsedTime();
256 | this.sfx.play('shot');
257 | }
258 |
259 | pointAtEnemy(){
260 | //Aim at enemy
261 | this.object.getWorldQuaternion(this.tmpQuat);
262 | this.object.lookAt( this.enemy.position );
263 | this.object.quaternion.slerp(this.tmpQuat, 0.9);
264 | }
265 |
266 | seeEnemy(){
267 | const enemyVec = this.enemy.position.clone().sub(this.object.position);
268 | const enemyDistance = enemyVec.length();
269 | enemyVec.normalize();
270 |
271 | this.aim.getWorldPosition(this.tmpVec);
272 | this.raycaster.set(this.tmpVec, enemyVec);
273 |
274 | const intersects = this.raycaster.intersectObjects( this.app.factory.children );
275 |
276 | if (intersects.length>0){
277 | return intersects[0].distance > enemyDistance;
278 | }
279 |
280 | return true;
281 | }
282 |
283 | update(dt){
284 | const speed = (this.actionName=='firingwalk') ? this.speed*0.6 : this.speed;
285 | const player = this.object;
286 |
287 | if (this.mixer) this.mixer.update(dt);
288 |
289 | if (this.rotateRifle !== undefined){
290 | this.rotateRifle.time += dt;
291 | if (this.rotateRifle.time > 0.5){
292 | this.rifle.quaternion.copy( this.rotateRifle.end );
293 | delete this.rotateRifle;
294 | }else{
295 | this.rifle.quaternion.slerpQuaternions(this.rotateRifle.start, this.rotateRifle.end, this.rotateRifle.time * 2);
296 | }
297 | }
298 |
299 | if (!this.dead && this.app.active && this.enemy && !this.enemy.userData.dead){
300 | if (!this.aggro){
301 | //Not in aggro mode so check enemy is in range and in sight
302 | if (this.withinAggroRange()){
303 | //Less than 20 metres away
304 | if (this.withinFOV(120)){
305 | //Within a 120 deg FOV
306 | this.aggro = true;
307 | const v = this.enemy.position.clone().sub(this.object.position);
308 | const len = v.length();
309 | if (len>10){
310 | this.newPath(this.enemy.position);
311 | }else{
312 | delete this.calculatedPath;
313 | this.action = 'idle';
314 | }
315 | }
316 | }
317 | }else{
318 | //Check still in aggro andrange
319 | if (this.withinAggroRange()){
320 | const v = this.enemy.position.clone().sub(this.object.position);
321 | const len = v.length();
322 | if (!this.isFiring){
323 | if (len<10){
324 | delete this.calculatedPath;
325 | this.firing = true;
326 | this.action = 'firing';
327 | }else if (this.withinFOV(10)){
328 | this.firing = true;
329 | }
330 | }else{
331 | if (!this.calculatedPath){
332 | this.pointAtEnemy();
333 | }else if (!this.withinFOV(10)){
334 | this.isFiring = false;
335 | this.action = 'walking';
336 | }
337 | if (this.isFiring && this.seeEnemy()){
338 | const elapsedTime = this.app.clock.getElapsedTime() - this.bulletTime;
339 | if (elapsedTime > 0.6) this.shoot();
340 | }
341 | }
342 | }else{
343 | this.firing = false;
344 | this.aggro = false;
345 | }
346 | }
347 | }else if (this.isFiring){
348 | this.firing = false;
349 | this.aggro = false;
350 | }
351 |
352 | if (this.calculatedPath && this.calculatedPath.length) {
353 | const targetPosition = this.calculatedPath[0];
354 |
355 | const vel = targetPosition.clone().sub(player.position);
356 |
357 | let pathLegComplete = (vel.lengthSq()<0.01);
358 |
359 | if (!pathLegComplete) {
360 | //Get the distance to the target before moving
361 | const prevDistanceSq = player.position.distanceToSquared(targetPosition);
362 | vel.normalize();
363 | // Move player to target
364 | if (this.quaternion) player.quaternion.slerp(this.quaternion, 0.1);
365 | player.position.add(vel.multiplyScalar(dt * speed));
366 | //Get distance after moving, if greater then we've overshot and this leg is complete
367 | const newDistanceSq = player.position.distanceToSquared(targetPosition);
368 | pathLegComplete = (newDistanceSq > prevDistanceSq);
369 | }
370 |
371 | if (pathLegComplete){
372 | // Remove node from the path we calculated
373 | this.calculatedPath.shift();
374 | if (this.calculatedPath.length==0){
375 | if (this.waypoints!==undefined){
376 | this.newPath(this.randomWaypoint);
377 | }else{
378 | player.position.copy( targetPosition );
379 | this.action = 'idle';
380 | }
381 | }else{
382 | this.setTargetDirection( this.calculatedPath[0].clone() );
383 | }
384 | }
385 | }else{
386 | if (!this.dead && this.waypoints!==undefined && !this.aggro) this.newPath(this.randomWaypoint);
387 | }
388 | }
389 | }
390 |
391 | export { NPC };
--------------------------------------------------------------------------------
/projects/shootout/NPCHandler.js:
--------------------------------------------------------------------------------
1 | import { NPC } from './NPC.js';
2 | import { GLTFLoader } from './three128/GLTFLoader.js';
3 | import { DRACOLoader } from './three128/DRACOLoader.js';
4 | import {
5 | Skeleton,
6 | Raycaster,
7 | BufferGeometry,
8 | Line,
9 | Vector3,
10 | } from './three128/three.module.js';
11 |
12 | class NPCHandler {
13 | constructor(game) {
14 | this.game = game;
15 | this.loadingBar = this.game.loadingBar;
16 | this.ready = false;
17 | this.load();
18 | }
19 |
20 | initMouseHandler() {
21 | const raycaster = new Raycaster();
22 | this.game.renderer.domElement.addEventListener('click', raycast, false);
23 |
24 | const self = this;
25 | const mouse = { x: 0, y: 0 };
26 |
27 | function raycast(e) {
28 | mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
29 | mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
30 |
31 | //2. set the picking ray from the camera position and mouse coordinates
32 | raycaster.setFromCamera(mouse, self.game.camera);
33 |
34 | //3. compute intersections
35 | const intersects = raycaster.intersectObject(self.game.navmesh);
36 |
37 | if (intersects.length > 0) {
38 | const pt = intersects[0].point;
39 | console.log(pt);
40 | self.npcs[0].newPath(pt, true);
41 | }
42 | }
43 | }
44 |
45 | reset() {
46 | this.npcs.forEach((npc) => {
47 | npc.reset();
48 | });
49 | }
50 |
51 | load() {
52 | const loader = new GLTFLoader().setPath(
53 | `${this.game.assetsPath}factory/`
54 | );
55 | const dracoLoader = new DRACOLoader();
56 | dracoLoader.setDecoderPath('../../assets/draco/');
57 | loader.setDRACOLoader(dracoLoader);
58 | this.loadingBar.visible = true;
59 |
60 | // Load a GLTF resource
61 | loader.load(
62 | // resource URL
63 | `swat-guy2.glb`,
64 | // called when the resource is loaded
65 | (gltf) => {
66 | if (this.game.pathfinder) {
67 | this.initNPCs(gltf);
68 | } else {
69 | this.gltf = gltf;
70 | }
71 | },
72 | // called while loading is progressing
73 | (xhr) => {
74 | this.loadingBar.update('swat-guy', xhr.loaded, xhr.total);
75 | },
76 | // called when loading has errors
77 | (err) => {
78 | console.error(err);
79 | }
80 | );
81 | }
82 |
83 | initNPCs(gltf = this.gltf) {
84 | this.waypoints = this.game.waypoints;
85 |
86 | const gltfs = [gltf];
87 |
88 | for (let i = 0; i < 3; i++) gltfs.push(this.cloneGLTF(gltf));
89 |
90 | this.npcs = [];
91 |
92 | gltfs.forEach((gltf) => {
93 | const object = gltf.scene;
94 | let rifle, aim;
95 |
96 | object.traverse(function (child) {
97 | if (child.isMesh) {
98 | child.castShadow = true;
99 | child.frustumCulled = false;
100 | if (child.name.includes('Rifle')) rifle = child;
101 | }
102 | });
103 |
104 | if (rifle) {
105 | const geometry = new BufferGeometry().setFromPoints([
106 | new Vector3(0, 0, 0),
107 | new Vector3(1, 0, 0),
108 | ]);
109 |
110 | const line = new Line(geometry);
111 | line.name = 'aim';
112 | line.scale.x = 50;
113 |
114 | rifle.add(line);
115 | line.position.set(0, 0, 0.5);
116 | aim = line;
117 | line.visible = false;
118 | }
119 |
120 | const options = {
121 | object,
122 | speed: 0.8,
123 | animations: gltf.animations,
124 | waypoints: this.waypoints,
125 | app: this.game,
126 | showPath: false,
127 | zone: 'factory',
128 | name: 'swat-guy',
129 | rifle,
130 | aim,
131 | };
132 |
133 | const npc = new NPC(options);
134 |
135 | npc.object.position.copy(this.randomWaypoint);
136 | npc.newPath(this.randomWaypoint);
137 |
138 | this.npcs.push(npc);
139 | });
140 |
141 | this.loadingBar.visible = !this.loadingBar.loaded;
142 | this.ready = true;
143 |
144 | this.game.startRendering();
145 | }
146 |
147 | cloneGLTF(gltf) {
148 | const clone = {
149 | animations: gltf.animations,
150 | scene: gltf.scene.clone(true),
151 | };
152 |
153 | const skinnedMeshes = {};
154 |
155 | gltf.scene.traverse((node) => {
156 | if (node.isSkinnedMesh) {
157 | skinnedMeshes[node.name] = node;
158 | }
159 | });
160 |
161 | const cloneBones = {};
162 | const cloneSkinnedMeshes = {};
163 |
164 | clone.scene.traverse((node) => {
165 | if (node.isBone) {
166 | cloneBones[node.name] = node;
167 | }
168 | if (node.isSkinnedMesh) {
169 | cloneSkinnedMeshes[node.name] = node;
170 | }
171 | });
172 |
173 | for (let name in skinnedMeshes) {
174 | const skinnedMesh = skinnedMeshes[name];
175 | const skeleton = skinnedMesh.skeleton;
176 | const cloneSkinnedMesh = cloneSkinnedMeshes[name];
177 | const orderedCloneBones = [];
178 | for (let i = 0; i < skeleton.bones.length; ++i) {
179 | const cloneBone = cloneBones[skeleton.bones[i].name];
180 | orderedCloneBones.push(cloneBone);
181 | }
182 | cloneSkinnedMesh.bind(
183 | new Skeleton(orderedCloneBones, skeleton.boneInverses),
184 | cloneSkinnedMesh.matrixWorld
185 | );
186 | }
187 |
188 | return clone;
189 | }
190 |
191 | get randomWaypoint() {
192 | const index = Math.floor(Math.random() * this.waypoints.length);
193 | return this.waypoints[index];
194 | }
195 |
196 | update(dt) {
197 | if (this.npcs) this.npcs.forEach((npc) => npc.update(dt));
198 | }
199 | }
200 |
201 | export { NPCHandler };
202 |
--------------------------------------------------------------------------------
/projects/shootout/UI.js:
--------------------------------------------------------------------------------
1 | class UI{
2 | constructor(game){
3 | const playBtn = document.getElementById('playBtn');
4 | playBtn.addEventListener('click', this.playBtnPressed.bind(this));
5 |
6 | this.game = game;
7 | }
8 |
9 | set visible(value){
10 | const playBtn = document.getElementById('playBtn');
11 | const ui = document.getElementById('ui');
12 | const display = (value) ? 'block' : 'none';
13 | playBtn.style.display = display;
14 | ui.style.display = display;
15 | }
16 |
17 | playBtnPressed(){
18 | const playBtn = document.getElementById('playBtn');
19 | playBtn.style.display = 'none';
20 | const img = playBtn.getElementsByTagName('img')[0];
21 | img.src = '../../assets/factory/playagain.png';
22 | this.game.startGame();
23 | }
24 |
25 | showGameover(){
26 | const gameover = document.getElementById('gameover');
27 | gameover.style.display = 'block';
28 |
29 | setTimeout(hideGameover, 2000);
30 |
31 | function hideGameover(){
32 | gameover.style.display = 'none';
33 | const playBtn = document.getElementById('playBtn');
34 | playBtn.style.display = 'block';
35 | }
36 | }
37 |
38 | set ammo(value){
39 | const progressBar = document.getElementsByName('ammoBar')[0];
40 | const percent = `${value * 100}%`;
41 | progressBar.style.width = percent;
42 | }
43 |
44 | set health(value){
45 | const progressBar = document.getElementsByName('healthBar')[0];
46 | const percent = `${value * 100}%`;
47 | progressBar.style.width = percent;
48 | }
49 | }
50 |
51 | export { UI };
--------------------------------------------------------------------------------
/projects/shootout/User.js:
--------------------------------------------------------------------------------
1 | import {
2 | Group,
3 | Vector3,
4 | Quaternion,
5 | Raycaster,
6 | AnimationMixer,
7 | SphereGeometry,
8 | MeshBasicMaterial,
9 | Mesh,
10 | BufferGeometry,
11 | Line,
12 | LoopOnce,
13 | } from './three128/three.module.js';
14 | import { GLTFLoader } from './three128/GLTFLoader.js';
15 | import { DRACOLoader } from './three128/DRACOLoader.js';
16 | import { SFX } from '@/libs/SFX.js';
17 |
18 | class User {
19 | constructor(game, pos, heading) {
20 | this.root = new Group();
21 | this.root.position.copy(pos);
22 | this.root.rotation.set(0, heading, 0, 'XYZ');
23 |
24 | this.startInfo = { pos: pos.clone(), heading };
25 |
26 | this.game = game;
27 |
28 | this.camera = game.camera;
29 | this.raycaster = new Raycaster();
30 |
31 | game.scene.add(this.root);
32 |
33 | this.loadingBar = game.loadingBar;
34 |
35 | this.load();
36 |
37 | this.tmpVec = new Vector3();
38 | this.tmpQuat = new Quaternion();
39 |
40 | this.speed = 0;
41 | this.isFiring = false;
42 |
43 | this.ready = false;
44 |
45 | //this.initMouseHandler();
46 | this.initRifleDirection();
47 | }
48 |
49 | initRifleDirection() {
50 | this.rifleDirection = {};
51 |
52 | this.rifleDirection.idle = new Quaternion(-0.178, -0.694, 0.667, 0.203);
53 | this.rifleDirection.walk = new Quaternion(0.044, -0.772, 0.626, -0.102);
54 | this.rifleDirection.firingwalk = new Quaternion(
55 | -0.034,
56 | -0.756,
57 | 0.632,
58 | -0.169
59 | );
60 | this.rifleDirection.firing = new Quaternion(
61 | -0.054,
62 | -0.75,
63 | 0.633,
64 | -0.184
65 | );
66 | this.rifleDirection.run = new Quaternion(0.015, -0.793, 0.595, -0.131);
67 | this.rifleDirection.shot = new Quaternion(
68 | -0.082,
69 | -0.789,
70 | 0.594,
71 | -0.138
72 | );
73 | }
74 |
75 | initMouseHandler() {
76 | this.game.renderer.domElement.addEventListener('click', raycast, false);
77 |
78 | const self = this;
79 | const mouse = { x: 0, y: 0 };
80 |
81 | function raycast(e) {
82 | mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
83 | mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
84 |
85 | //2. set the picking ray from the camera position and mouse coordinates
86 | self.raycaster.setFromCamera(mouse, self.game.camera);
87 |
88 | //3. compute intersections
89 | const intersects = self.raycaster.intersectObject(
90 | self.game.navmesh
91 | );
92 |
93 | if (intersects.length > 0) {
94 | const pt = intersects[0].point;
95 | console.log(pt);
96 |
97 | self.root.position.copy(pt);
98 |
99 | self.root.remove(self.dolly);
100 |
101 | self.dolly.position.copy(self.game.camera.position);
102 | self.dolly.quaternion.copy(self.game.camera.quaternion);
103 |
104 | self.root.attach(self.dolly);
105 | }
106 | }
107 | }
108 |
109 | reset() {
110 | this.position = this.startInfo.pos;
111 | this.root.rotation.set(0, this.startInfo.heading, 0, 'XYZ');
112 | this.root.rotateY(0.7);
113 | this.root.userData.dead = false;
114 | this.ammo = 100;
115 | this.health = 100;
116 | this.action = 'idle';
117 | this.dead = false;
118 | this.speed = 0;
119 | this.isFiring = false;
120 | }
121 |
122 | set position(pos) {
123 | this.root.position.copy(pos);
124 | }
125 |
126 | get position() {
127 | return this.root.position;
128 | }
129 |
130 | set firing(mode) {
131 | this.isFiring = mode;
132 | if (mode) {
133 | this.action = Math.abs(this.speed) == 0 ? 'firing' : 'firingwalk';
134 | this.bulletTime = this.game.clock.getElapsedTime();
135 | } else {
136 | this.action = 'idle';
137 | }
138 | }
139 |
140 | shoot() {
141 | if (this.ammo < 1) return;
142 | if (this.bulletHandler === undefined)
143 | this.bulletHandler = this.game.bulletHandler;
144 | this.aim.getWorldPosition(this.tmpVec);
145 | this.aim.getWorldQuaternion(this.tmpQuat);
146 | this.bulletHandler.createBullet(this.tmpVec, this.tmpQuat);
147 | this.bulletTime = this.game.clock.getElapsedTime();
148 | this.ammo--;
149 | this.game.ui.ammo = Math.max(0, Math.min(this.ammo / 100, 1));
150 | this.sfx.play('shot');
151 | }
152 |
153 | addSphere() {
154 | const geometry = new SphereGeometry(0.1, 8, 8);
155 | const material = new MeshBasicMaterial({ color: 0xff0000 });
156 | const mesh = new Mesh(geometry, material);
157 | this.game.scene.add(mesh);
158 | this.hitPoint = mesh;
159 | this.hitPoint.visible = false;
160 | }
161 |
162 | load() {
163 | const loader = new GLTFLoader().setPath(
164 | `${this.game.assetsPath}factory/`
165 | );
166 | const dracoLoader = new DRACOLoader();
167 | dracoLoader.setDecoderPath('../../assets/draco/');
168 | loader.setDRACOLoader(dracoLoader);
169 |
170 | // Load a glTF resource
171 | loader.load(
172 | // resource URL
173 | 'eve2.glb',
174 | // called when the resource is loaded
175 | (gltf) => {
176 | this.root.add(gltf.scene);
177 | this.object = gltf.scene;
178 | this.object.frustumCulled = false;
179 |
180 | const scale = 1.2;
181 | this.object.scale.set(scale, scale, scale);
182 |
183 | this.object.traverse((child) => {
184 | if (child.isMesh) {
185 | child.castShadow = true;
186 | child.frustumCulled = false;
187 | if (child.name.includes('Rifle')) this.rifle = child;
188 | }
189 | });
190 |
191 | if (this.rifle) {
192 | const geometry = new BufferGeometry().setFromPoints([
193 | new Vector3(0, 0, 0),
194 | new Vector3(1, 0, 0),
195 | ]);
196 |
197 | const line = new Line(geometry);
198 | line.name = 'aim';
199 | line.scale.x = 50;
200 |
201 | this.rifle.add(line);
202 | line.position.set(0, 0, 0.5);
203 | this.aim = line;
204 | line.visible = false;
205 | }
206 |
207 | this.animations = {};
208 |
209 | gltf.animations.forEach((animation) => {
210 | this.animations[animation.name.toLowerCase()] = animation;
211 | });
212 |
213 | this.mixer = new AnimationMixer(gltf.scene);
214 |
215 | this.action = 'idle';
216 |
217 | this.ready = true;
218 |
219 | this.game.startRendering();
220 | },
221 | // called while loading is progressing
222 | (xhr) => {
223 | this.loadingBar.update('user', xhr.loaded, xhr.total);
224 | },
225 | // called when loading has errors
226 | (err) => {
227 | console.error(err);
228 | }
229 | );
230 | }
231 |
232 | initSounds() {
233 | const assetsPath = `${this.game.assetsPath}factory/sfx/`;
234 | this.sfx = new SFX(this.game.camera, assetsPath, this.game.listener);
235 | this.sfx.load('footsteps', true, 0.8, this.object);
236 | this.sfx.load('eve-groan', false, 0.8, this.object);
237 | this.sfx.load('shot', false, 0.8, this.object);
238 | }
239 |
240 | set action(name) {
241 | name = name.toLowerCase();
242 | if (this.actionName == name) return;
243 |
244 | //console.log(`User action:${name}`);
245 | if (name == 'shot') {
246 | this.health -= 25;
247 | if (this.health >= 0) {
248 | name = 'hit';
249 | //Temporarily disable control
250 | this.game.active = false;
251 | setTimeout(() => (this.game.active = true), 1000);
252 | }
253 | this.game.tintScreen(name);
254 | this.game.ui.health = Math.max(0, Math.min(this.health / 100, 1));
255 | if (this.sfx) this.sfx.play('eve-groan');
256 | }
257 |
258 | if (this.sfx) {
259 | if (name == 'walk' || name == 'firingwalk' || name == 'run') {
260 | this.sfx.play('footsteps');
261 | } else {
262 | this.sfx.stop('footsteps');
263 | }
264 | }
265 |
266 | const clip = this.animations[name.toLowerCase()];
267 |
268 | if (clip !== undefined) {
269 | const action = this.mixer.clipAction(clip);
270 | if (name == 'shot') {
271 | action.clampWhenFinished = true;
272 | action.setLoop(LoopOnce);
273 | this.dead = true;
274 | this.root.userData.dead = true;
275 | this.game.gameover();
276 | }
277 | action.reset();
278 | const nofade = this.actionName == 'shot';
279 | this.actionName = name.toLowerCase();
280 | action.play();
281 | if (this.curAction) {
282 | if (nofade) {
283 | this.curAction.enabled = false;
284 | } else {
285 | this.curAction.crossFadeTo(action, 0.5);
286 | }
287 | }
288 | this.curAction = action;
289 | }
290 | if (this.rifle && this.rifleDirection) {
291 | const q = this.rifleDirection[name.toLowerCase()];
292 | if (q !== undefined) {
293 | const start = new Quaternion();
294 | start.copy(this.rifle.quaternion);
295 | this.rifle.quaternion.copy(q);
296 | this.rifle.rotateX(1.57);
297 | const end = new Quaternion();
298 | end.copy(this.rifle.quaternion);
299 | this.rotateRifle = { start, end, time: 0 };
300 | this.rifle.quaternion.copy(start);
301 | }
302 | }
303 | }
304 |
305 | update(dt) {
306 | if (this.mixer) this.mixer.update(dt);
307 | if (this.rotateRifle !== undefined) {
308 | this.rotateRifle.time += dt;
309 | if (this.rotateRifle.time > 0.5) {
310 | this.rifle.quaternion.copy(this.rotateRifle.end);
311 | delete this.rotateRifle;
312 | } else {
313 | this.rifle.quaternion.slerpQuaternions(
314 | this.rotateRifle.start,
315 | this.rotateRifle.end,
316 | this.rotateRifle.time * 2
317 | );
318 | }
319 | }
320 | if (this.isFiring) {
321 | const elapsedTime =
322 | this.game.clock.getElapsedTime() - this.bulletTime;
323 | if (elapsedTime > 0.6) this.shoot();
324 | }
325 | }
326 | }
327 |
328 | export { User };
329 |
--------------------------------------------------------------------------------
/projects/shootout/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
84 |
85 | Shootout
86 |
87 |
88 |
89 |
97 |
98 |
99 |
100 |
101 |
102 |
105 |
106 |
107 |
110 |
111 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/projects/shootout/pathfinding/AStar.js:
--------------------------------------------------------------------------------
1 | import { BinaryHeap } from './BinaryHeap.js';
2 | import { Utils } from './Utils.js';
3 |
4 | class AStar {
5 | static init (graph) {
6 | for (let x = 0; x < graph.length; x++) {
7 | //for(var x in graph) {
8 | const node = graph[x];
9 | node.f = 0;
10 | node.g = 0;
11 | node.h = 0;
12 | node.cost = 1.0;
13 | node.visited = false;
14 | node.closed = false;
15 | node.parent = null;
16 | }
17 | }
18 |
19 | static cleanUp (graph) {
20 | for (let x = 0; x < graph.length; x++) {
21 | const node = graph[x];
22 | delete node.f;
23 | delete node.g;
24 | delete node.h;
25 | delete node.cost;
26 | delete node.visited;
27 | delete node.closed;
28 | delete node.parent;
29 | }
30 | }
31 |
32 | static heap () {
33 | return new BinaryHeap(function (node) {
34 | return node.f;
35 | });
36 | }
37 |
38 | static search (graph, start, end) {
39 | this.init(graph);
40 | //heuristic = heuristic || astar.manhattan;
41 |
42 |
43 | const openHeap = this.heap();
44 |
45 | openHeap.push(start);
46 |
47 | while (openHeap.size() > 0) {
48 |
49 | // Grab the lowest f(x) to process next. Heap keeps this sorted for us.
50 | const currentNode = openHeap.pop();
51 |
52 | // End case -- result has been found, return the traced path.
53 | if (currentNode === end) {
54 | let curr = currentNode;
55 | const ret = [];
56 | while (curr.parent) {
57 | ret.push(curr);
58 | curr = curr.parent;
59 | }
60 | this.cleanUp(ret);
61 | return ret.reverse();
62 | }
63 |
64 | // Normal case -- move currentNode from open to closed, process each of its neighbours.
65 | currentNode.closed = true;
66 |
67 | // Find all neighbours for the current node. Optionally find diagonal neighbours as well (false by default).
68 | const neighbours = this.neighbours(graph, currentNode);
69 |
70 | for (let i = 0, il = neighbours.length; i < il; i++) {
71 | const neighbour = neighbours[i];
72 |
73 | if (neighbour.closed) {
74 | // Not a valid node to process, skip to next neighbour.
75 | continue;
76 | }
77 |
78 | // The g score is the shortest distance from start to current node.
79 | // We need to check if the path we have arrived at this neighbour is the shortest one we have seen yet.
80 | const gScore = currentNode.g + neighbour.cost;
81 | const beenVisited = neighbour.visited;
82 |
83 | if (!beenVisited || gScore < neighbour.g) {
84 |
85 | // Found an optimal (so far) path to this node. Take score for node to see how good it is.
86 | neighbour.visited = true;
87 | neighbour.parent = currentNode;
88 | if (!neighbour.centroid || !end.centroid) throw new Error('Unexpected state');
89 | neighbour.h = neighbour.h || this.heuristic(neighbour.centroid, end.centroid);
90 | neighbour.g = gScore;
91 | neighbour.f = neighbour.g + neighbour.h;
92 |
93 | if (!beenVisited) {
94 | // Pushing to heap will put it in proper place based on the 'f' value.
95 | openHeap.push(neighbour);
96 | } else {
97 | // Already seen the node, but since it has been rescored we need to reorder it in the heap
98 | openHeap.rescoreElement(neighbour);
99 | }
100 | }
101 | }
102 | }
103 |
104 | // No result was found - empty array signifies failure to find path.
105 | return [];
106 | }
107 |
108 | static heuristic (pos1, pos2) {
109 | return Utils.distanceToSquared(pos1, pos2);
110 | }
111 |
112 | static neighbours (graph, node) {
113 | const ret = [];
114 |
115 | for (let e = 0; e < node.neighbours.length; e++) {
116 | ret.push(graph[node.neighbours[e]]);
117 | }
118 |
119 | return ret;
120 | }
121 | }
122 |
123 | export { AStar };
124 |
--------------------------------------------------------------------------------
/projects/shootout/pathfinding/BinaryHeap.js:
--------------------------------------------------------------------------------
1 | // javascript-astar
2 | // http://github.com/bgrins/javascript-astar
3 | // Freely distributable under the MIT License.
4 | // Implements the astar search algorithm in javascript using a binary heap.
5 |
6 | class BinaryHeap {
7 | constructor (scoreFunction) {
8 | this.content = [];
9 | this.scoreFunction = scoreFunction;
10 | }
11 |
12 | push (element) {
13 | // Add the new element to the end of the array.
14 | this.content.push(element);
15 |
16 | // Allow it to sink down.
17 | this.sinkDown(this.content.length - 1);
18 | }
19 |
20 | pop () {
21 | // Store the first element so we can return it later.
22 | const result = this.content[0];
23 | // Get the element at the end of the array.
24 | const end = this.content.pop();
25 | // If there are any elements left, put the end element at the
26 | // start, and let it bubble up.
27 | if (this.content.length > 0) {
28 | this.content[0] = end;
29 | this.bubbleUp(0);
30 | }
31 | return result;
32 | }
33 |
34 | remove (node) {
35 | const i = this.content.indexOf(node);
36 |
37 | // When it is found, the process seen in 'pop' is repeated
38 | // to fill up the hole.
39 | const end = this.content.pop();
40 |
41 | if (i !== this.content.length - 1) {
42 | this.content[i] = end;
43 |
44 | if (this.scoreFunction(end) < this.scoreFunction(node)) {
45 | this.sinkDown(i);
46 | } else {
47 | this.bubbleUp(i);
48 | }
49 | }
50 | }
51 |
52 | size () {
53 | return this.content.length;
54 | }
55 |
56 | rescoreElement (node) {
57 | this.sinkDown(this.content.indexOf(node));
58 | }
59 |
60 | sinkDown (n) {
61 | // Fetch the element that has to be sunk.
62 | const element = this.content[n];
63 |
64 | // When at 0, an element can not sink any further.
65 | while (n > 0) {
66 | // Compute the parent element's index, and fetch it.
67 | const parentN = ((n + 1) >> 1) - 1;
68 | const parent = this.content[parentN];
69 |
70 | if (this.scoreFunction(element) < this.scoreFunction(parent)) {
71 | // Swap the elements if the parent is greater.
72 | this.content[parentN] = element;
73 | this.content[n] = parent;
74 | // Update 'n' to continue at the new position.
75 | n = parentN;
76 | } else {
77 | // Found a parent that is less, no need to sink any further.
78 | break;
79 | }
80 | }
81 | }
82 |
83 | bubbleUp (n) {
84 | // Look up the target element and its score.
85 | const length = this.content.length,
86 | element = this.content[n],
87 | elemScore = this.scoreFunction(element);
88 |
89 | while (true) {
90 | // Compute the indices of the child elements.
91 | const child2N = (n + 1) << 1,
92 | child1N = child2N - 1;
93 | // This is used to store the new position of the element,
94 | // if any.
95 | let swap = null;
96 | let child1Score;
97 | // If the first child exists (is inside the array)...
98 | if (child1N < length) {
99 | // Look it up and compute its score.
100 | const child1 = this.content[child1N];
101 | child1Score = this.scoreFunction(child1);
102 |
103 | // If the score is less than our element's, we need to swap.
104 | if (child1Score < elemScore) {
105 | swap = child1N;
106 | }
107 | }
108 |
109 | // Do the same checks for the other child.
110 | if (child2N < length) {
111 | const child2 = this.content[child2N],
112 | child2Score = this.scoreFunction(child2);
113 | if (child2Score < (swap === null ? elemScore : child1Score)) {
114 | swap = child2N;
115 | }
116 | }
117 |
118 | // If the element needs to be moved, swap it, and continue.
119 | if (swap !== null) {
120 | this.content[n] = this.content[swap];
121 | this.content[swap] = element;
122 | n = swap;
123 | }
124 |
125 | // Otherwise, we are done.
126 | else {
127 | break;
128 | }
129 | }
130 | }
131 |
132 | }
133 |
134 | export { BinaryHeap };
135 |
--------------------------------------------------------------------------------
/projects/shootout/pathfinding/Builder.js:
--------------------------------------------------------------------------------
1 | import { Vector3 } from '../three128/three.module.js';
2 |
3 | import { Utils } from './Utils.js';
4 |
5 | class Builder {
6 | /**
7 | * Constructs groups from the given navigation mesh.
8 | * @param {BufferGeometry} geometry
9 | * @return {Zone}
10 | */
11 | static buildZone (geometry, tolerance = 1e-4) {
12 |
13 | const navMesh = this._buildNavigationMesh(geometry, tolerance);
14 |
15 | const zone = {};
16 |
17 | navMesh.vertices.forEach((v) => {
18 | v.x = Utils.roundNumber(v.x, 2);
19 | v.y = Utils.roundNumber(v.y, 2);
20 | v.z = Utils.roundNumber(v.z, 2);
21 | });
22 |
23 | zone.vertices = navMesh.vertices;
24 |
25 | const groups = this._buildPolygonGroups(navMesh);
26 |
27 | // TODO: This block represents a large portion of navigation mesh construction time
28 | // and could probably be optimized. For example, construct portals while
29 | // determining the neighbor graph.
30 | zone.groups = new Array(groups.length);
31 | groups.forEach((group, groupIndex) => {
32 |
33 | const indexByPolygon = new Map(); // { polygon: index in group }
34 | group.forEach((poly, polyIndex) => { indexByPolygon.set(poly, polyIndex); });
35 |
36 | const newGroup = new Array(group.length);
37 | group.forEach((poly, polyIndex) => {
38 |
39 | const neighbourIndices = [];
40 | poly.neighbours.forEach((n) => neighbourIndices.push(indexByPolygon.get(n)));
41 |
42 | // Build a portal list to each neighbour
43 | const portals = [];
44 | poly.neighbours.forEach((n) => portals.push(this._getSharedVerticesInOrder(poly, n)));
45 |
46 | const centroid = new Vector3( 0, 0, 0 );
47 | centroid.add( zone.vertices[ poly.vertexIds[0] ] );
48 | centroid.add( zone.vertices[ poly.vertexIds[1] ] );
49 | centroid.add( zone.vertices[ poly.vertexIds[2] ] );
50 | centroid.divideScalar( 3 );
51 | centroid.x = Utils.roundNumber(centroid.x, 2);
52 | centroid.y = Utils.roundNumber(centroid.y, 2);
53 | centroid.z = Utils.roundNumber(centroid.z, 2);
54 |
55 | newGroup[polyIndex] = {
56 | id: polyIndex,
57 | neighbours: neighbourIndices,
58 | vertexIds: poly.vertexIds,
59 | centroid: centroid,
60 | portals: portals
61 | };
62 | });
63 |
64 | zone.groups[groupIndex] = newGroup;
65 | });
66 |
67 | return zone;
68 | }
69 |
70 | /**
71 | * Constructs a navigation mesh from the given geometry.
72 | * @param {BufferGeometry} geometry
73 | * @return {Object}
74 | */
75 | static _buildNavigationMesh (geometry, tolerance) {
76 | geometry = Utils.prepGeometry(geometry, tolerance);
77 | return this._buildPolygonsFromGeometry(geometry);
78 | }
79 |
80 | static _buildPolygonGroups (navigationMesh) {
81 |
82 | const polygons = navigationMesh.polygons;
83 |
84 | const polygonGroups = [];
85 |
86 | const spreadGroupId = function (polygon) {
87 | polygon.neighbours.forEach((neighbour) => {
88 | if (neighbour.group === undefined) {
89 | neighbour.group = polygon.group;
90 | spreadGroupId(neighbour);
91 | }
92 | });
93 | };
94 |
95 | polygons.forEach((polygon) => {
96 | if (polygon.group !== undefined) {
97 | // this polygon is already part of a group
98 | polygonGroups[polygon.group].push(polygon);
99 | } else {
100 | // we need to make a new group and spread its ID to neighbors
101 | polygon.group = polygonGroups.length;
102 | spreadGroupId(polygon);
103 | polygonGroups.push([polygon]);
104 | }
105 | });
106 |
107 | return polygonGroups;
108 | }
109 |
110 | static _buildPolygonNeighbours (polygon, vertexPolygonMap) {
111 | const neighbours = new Set();
112 |
113 | const groupA = vertexPolygonMap[polygon.vertexIds[0]];
114 | const groupB = vertexPolygonMap[polygon.vertexIds[1]];
115 | const groupC = vertexPolygonMap[polygon.vertexIds[2]];
116 |
117 | // It's only necessary to iterate groups A and B. Polygons contained only
118 | // in group C cannot share a >1 vertex with this polygon.
119 | // IMPORTANT: Bublé cannot compile for-of loops.
120 | groupA.forEach((candidate) => {
121 | if (candidate === polygon) return;
122 | if (groupB.includes(candidate) || groupC.includes(candidate)) {
123 | neighbours.add(candidate);
124 | }
125 | });
126 | groupB.forEach((candidate) => {
127 | if (candidate === polygon) return;
128 | if (groupC.includes(candidate)) {
129 | neighbours.add(candidate);
130 | }
131 | });
132 |
133 | return neighbours;
134 | }
135 |
136 | static _buildPolygonsFromGeometry (geometry) {
137 |
138 | const polygons = [];
139 | const vertices = [];
140 |
141 | const position = geometry.attributes.position;
142 | const index = geometry.index;
143 |
144 | // Constructing the neighbor graph brute force is O(n²). To avoid that,
145 | // create a map from vertices to the polygons that contain them, and use it
146 | // while connecting polygons. This reduces complexity to O(n*m), where 'm'
147 | // is related to connectivity of the mesh.
148 |
149 | /** Array of polygon objects by vertex index. */
150 | const vertexPolygonMap = [];
151 |
152 | for (let i = 0; i < position.count; i++) {
153 | vertices.push(new Vector3().fromBufferAttribute(position, i));
154 | vertexPolygonMap[i] = [];
155 | }
156 |
157 | // Convert the faces into a custom format that supports more than 3 vertices
158 | for (let i = 0; i < geometry.index.count; i += 3) {
159 | const a = index.getX(i);
160 | const b = index.getX(i + 1);
161 | const c = index.getX(i + 2);
162 | const poly = {vertexIds: [a, b, c], neighbours: null};
163 | polygons.push(poly);
164 | vertexPolygonMap[a].push(poly);
165 | vertexPolygonMap[b].push(poly);
166 | vertexPolygonMap[c].push(poly);
167 | }
168 |
169 | // Build a list of adjacent polygons
170 | polygons.forEach((polygon) => {
171 | polygon.neighbours = this._buildPolygonNeighbours(polygon, vertexPolygonMap);
172 | });
173 |
174 | return {
175 | polygons: polygons,
176 | vertices: vertices
177 | };
178 | }
179 |
180 | static _getSharedVerticesInOrder (a, b) {
181 |
182 | const aList = a.vertexIds;
183 | const a0 = aList[0], a1 = aList[1], a2 = aList[2];
184 |
185 | const bList = b.vertexIds;
186 | const shared0 = bList.includes(a0);
187 | const shared1 = bList.includes(a1);
188 | const shared2 = bList.includes(a2);
189 |
190 | // it seems that we shouldn't have an a and b with <2 shared vertices here unless there's a bug
191 | // in the neighbor identification code, or perhaps a malformed input geometry; 3 shared vertices
192 | // is a kind of embarrassing but possible geometry we should handle
193 | if (shared0 && shared1 && shared2) {
194 | return Array.from(aList);
195 | } else if (shared0 && shared1) {
196 | return [a0, a1];
197 | } else if (shared1 && shared2) {
198 | return [a1, a2];
199 | } else if (shared0 && shared2) {
200 | return [a2, a0]; // this ordering will affect the string pull algorithm later, not clear if significant
201 | } else {
202 | console.warn("Error processing navigation mesh neighbors; neighbors with <2 shared vertices found.");
203 | return [];
204 | }
205 | }
206 | }
207 |
208 | export { Builder };
209 |
--------------------------------------------------------------------------------
/projects/shootout/pathfinding/Channel.js:
--------------------------------------------------------------------------------
1 | import { Utils } from './Utils.js';
2 |
3 | class Channel {
4 | constructor () {
5 | this.portals = [];
6 | }
7 |
8 | push (p1, p2) {
9 | if (p2 === undefined) p2 = p1;
10 | this.portals.push({
11 | left: p1,
12 | right: p2
13 | });
14 | }
15 |
16 | stringPull () {
17 | const portals = this.portals;
18 | const pts = [];
19 | // Init scan state
20 | let portalApex, portalLeft, portalRight;
21 | let apexIndex = 0,
22 | leftIndex = 0,
23 | rightIndex = 0;
24 |
25 | portalApex = portals[0].left;
26 | portalLeft = portals[0].left;
27 | portalRight = portals[0].right;
28 |
29 | // Add start point.
30 | pts.push(portalApex);
31 |
32 | for (let i = 1; i < portals.length; i++) {
33 | const left = portals[i].left;
34 | const right = portals[i].right;
35 |
36 | // Update right vertex.
37 | if (Utils.triarea2(portalApex, portalRight, right) <= 0.0) {
38 | if (Utils.vequal(portalApex, portalRight) || Utils.triarea2(portalApex, portalLeft, right) > 0.0) {
39 | // Tighten the funnel.
40 | portalRight = right;
41 | rightIndex = i;
42 | } else {
43 | // Right over left, insert left to path and restart scan from portal left point.
44 | pts.push(portalLeft);
45 | // Make current left the new apex.
46 | portalApex = portalLeft;
47 | apexIndex = leftIndex;
48 | // Reset portal
49 | portalLeft = portalApex;
50 | portalRight = portalApex;
51 | leftIndex = apexIndex;
52 | rightIndex = apexIndex;
53 | // Restart scan
54 | i = apexIndex;
55 | continue;
56 | }
57 | }
58 |
59 | // Update left vertex.
60 | if (Utils.triarea2(portalApex, portalLeft, left) >= 0.0) {
61 | if (Utils.vequal(portalApex, portalLeft) || Utils.triarea2(portalApex, portalRight, left) < 0.0) {
62 | // Tighten the funnel.
63 | portalLeft = left;
64 | leftIndex = i;
65 | } else {
66 | // Left over right, insert right to path and restart scan from portal right point.
67 | pts.push(portalRight);
68 | // Make current right the new apex.
69 | portalApex = portalRight;
70 | apexIndex = rightIndex;
71 | // Reset portal
72 | portalLeft = portalApex;
73 | portalRight = portalApex;
74 | leftIndex = apexIndex;
75 | rightIndex = apexIndex;
76 | // Restart scan
77 | i = apexIndex;
78 | continue;
79 | }
80 | }
81 | }
82 |
83 | if ((pts.length === 0) || (!Utils.vequal(pts[pts.length - 1], portals[portals.length - 1].left))) {
84 | // Append last point to path.
85 | pts.push(portals[portals.length - 1].left);
86 | }
87 |
88 | this.path = pts;
89 | return pts;
90 | }
91 | }
92 |
93 | export { Channel };
94 |
--------------------------------------------------------------------------------
/projects/shootout/pathfinding/Pathfinding.js:
--------------------------------------------------------------------------------
1 | import {
2 | Vector3,
3 | Plane,
4 | Triangle,
5 | } from '../three128/three.module.js';
6 |
7 | import { Utils } from './Utils.js';
8 | import { AStar } from './AStar.js';
9 | import { Builder } from './Builder.js';
10 | import { Channel } from './Channel.js';
11 |
12 | /**
13 | * Defines an instance of the pathfinding module, with one or more zones.
14 | */
15 | class Pathfinding {
16 | constructor () {
17 | this.zones = {};
18 | }
19 |
20 | /**
21 | * (Static) Builds a zone/node set from navigation mesh geometry.
22 | * @param {BufferGeometry} geometry
23 | * @return {Zone}
24 | */
25 | static createZone (geometry, tolerance = 1e-4) {
26 | return Builder.buildZone(geometry, tolerance);
27 | }
28 |
29 | /**
30 | * Sets data for the given zone.
31 | * @param {string} zoneID
32 | * @param {Zone} zone
33 | */
34 | setZoneData (zoneID, zone) {
35 | this.zones[zoneID] = zone;
36 | }
37 |
38 | /**
39 | * Returns a random node within a given range of a given position.
40 | * @param {string} zoneID
41 | * @param {number} groupID
42 | * @param {Vector3} nearPosition
43 | * @param {number} nearRange
44 | * @return {Node}
45 | */
46 | getRandomNode (zoneID, groupID, nearPosition, nearRange) {
47 |
48 | if (!this.zones[zoneID]) return new Vector3();
49 |
50 | nearPosition = nearPosition || null;
51 | nearRange = nearRange || 0;
52 |
53 | const candidates = [];
54 | const polygons = this.zones[zoneID].groups[groupID];
55 |
56 | polygons.forEach((p) => {
57 | if (nearPosition && nearRange) {
58 | if (Utils.distanceToSquared(nearPosition, p.centroid) < nearRange * nearRange) {
59 | candidates.push(p.centroid);
60 | }
61 | } else {
62 | candidates.push(p.centroid);
63 | }
64 | });
65 |
66 | return Utils.sample(candidates) || new Vector3();
67 | }
68 |
69 | /**
70 | * Returns the closest node to the target position.
71 | * @param {Vector3} position
72 | * @param {string} zoneID
73 | * @param {number} groupID
74 | * @param {boolean} checkPolygon
75 | * @return {Node}
76 | */
77 | getClosestNode (position, zoneID, groupID, checkPolygon = false) {
78 | const nodes = this.zones[zoneID].groups[groupID];
79 | const vertices = this.zones[zoneID].vertices;
80 | let closestNode = null;
81 | let closestDistance = Infinity;
82 |
83 | nodes.forEach((node) => {
84 | const distance = Utils.distanceToSquared(node.centroid, position);
85 | if (distance < closestDistance
86 | && (!checkPolygon || Utils.isVectorInPolygon(position, node, vertices))) {
87 | closestNode = node;
88 | closestDistance = distance;
89 | }
90 | });
91 |
92 | return closestNode;
93 | }
94 |
95 | /**
96 | * Returns a path between given start and end points. If a complete path
97 | * cannot be found, will return the nearest endpoint available.
98 | *
99 | * @param {Vector3} startPosition Start position.
100 | * @param {Vector3} targetPosition Destination.
101 | * @param {string} zoneID ID of current zone.
102 | * @param {number} groupID Current group ID.
103 | * @return {Array} Array of points defining the path.
104 | */
105 | findPath (startPosition, targetPosition, zoneID, groupID) {
106 | const nodes = this.zones[zoneID].groups[groupID];
107 | const vertices = this.zones[zoneID].vertices;
108 |
109 | const closestNode = this.getClosestNode(startPosition, zoneID, groupID, true);
110 | const farthestNode = this.getClosestNode(targetPosition, zoneID, groupID, true);
111 |
112 | // If we can't find any node, just go straight to the target
113 | if (!closestNode || !farthestNode) {
114 | return null;
115 | }
116 |
117 | const paths = AStar.search(nodes, closestNode, farthestNode);
118 |
119 | const getPortalFromTo = function (a, b) {
120 | for (var i = 0; i < a.neighbours.length; i++) {
121 | if (a.neighbours[i] === b.id) {
122 | return a.portals[i];
123 | }
124 | }
125 | };
126 |
127 | // We have the corridor, now pull the rope.
128 | const channel = new Channel();
129 | channel.push(startPosition);
130 | for (let i = 0; i < paths.length; i++) {
131 | const polygon = paths[i];
132 | const nextPolygon = paths[i + 1];
133 |
134 | if (nextPolygon) {
135 | const portals = getPortalFromTo(polygon, nextPolygon);
136 | channel.push(
137 | vertices[portals[0]],
138 | vertices[portals[1]]
139 | );
140 | }
141 | }
142 | channel.push(targetPosition);
143 | channel.stringPull();
144 |
145 | // Return the path, omitting first position (which is already known).
146 | const path = channel.path.map((c) => new Vector3(c.x, c.y, c.z));
147 | path.shift();
148 | return path;
149 | }
150 | }
151 |
152 | /**
153 | * Returns closest node group ID for given position.
154 | * @param {string} zoneID
155 | * @param {Vector3} position
156 | * @return {number}
157 | */
158 | Pathfinding.prototype.getGroup = (function() {
159 | const plane = new Plane();
160 | return function (zoneID, position, checkPolygon = false) {
161 | if (!this.zones[zoneID]) return null;
162 |
163 | let closestNodeGroup = null;
164 | let distance = Math.pow(50, 2);
165 | const zone = this.zones[zoneID];
166 |
167 | for (let i = 0; i < zone.groups.length; i++) {
168 | const group = zone.groups[i];
169 | for (const node of group) {
170 | if (checkPolygon) {
171 | plane.setFromCoplanarPoints(
172 | zone.vertices[node.vertexIds[0]],
173 | zone.vertices[node.vertexIds[1]],
174 | zone.vertices[node.vertexIds[2]]
175 | );
176 | if (Math.abs(plane.distanceToPoint(position)) < 0.01) {
177 | const poly = [
178 | zone.vertices[node.vertexIds[0]],
179 | zone.vertices[node.vertexIds[1]],
180 | zone.vertices[node.vertexIds[2]]
181 | ];
182 | if(Utils.isPointInPoly(poly, position)) {
183 | return i;
184 | }
185 | }
186 | }
187 | const measuredDistance = Utils.distanceToSquared(node.centroid, position);
188 | if (measuredDistance < distance) {
189 | closestNodeGroup = i;
190 | distance = measuredDistance;
191 | }
192 | }
193 | }
194 |
195 | return closestNodeGroup;
196 | };
197 | }());
198 |
199 | /**
200 | * Clamps a step along the navmesh, given start and desired endpoint. May be
201 | * used to constrain first-person / WASD controls.
202 | *
203 | * @param {Vector3} start
204 | * @param {Vector3} end Desired endpoint.
205 | * @param {Node} node
206 | * @param {string} zoneID
207 | * @param {number} groupID
208 | * @param {Vector3} endTarget Updated endpoint.
209 | * @return {Node} Updated node.
210 | */
211 | Pathfinding.prototype.clampStep = (function () {
212 | const point = new Vector3();
213 | const plane = new Plane();
214 | const triangle = new Triangle();
215 |
216 | const endPoint = new Vector3();
217 |
218 | let closestNode;
219 | let closestPoint = new Vector3();
220 | let closestDistance;
221 |
222 | return function (startRef, endRef, node, zoneID, groupID, endTarget) {
223 | const vertices = this.zones[zoneID].vertices;
224 | const nodes = this.zones[zoneID].groups[groupID];
225 |
226 | const nodeQueue = [node];
227 | const nodeDepth = {};
228 | nodeDepth[node.id] = 0;
229 |
230 | closestNode = undefined;
231 | closestPoint.set(0, 0, 0);
232 | closestDistance = Infinity;
233 |
234 | // Project the step along the current node.
235 | plane.setFromCoplanarPoints(
236 | vertices[node.vertexIds[0]],
237 | vertices[node.vertexIds[1]],
238 | vertices[node.vertexIds[2]]
239 | );
240 | plane.projectPoint(endRef, point);
241 | endPoint.copy(point);
242 |
243 | for (let currentNode = nodeQueue.pop(); currentNode; currentNode = nodeQueue.pop()) {
244 |
245 | triangle.set(
246 | vertices[currentNode.vertexIds[0]],
247 | vertices[currentNode.vertexIds[1]],
248 | vertices[currentNode.vertexIds[2]]
249 | );
250 |
251 | triangle.closestPointToPoint(endPoint, point);
252 |
253 | if (point.distanceToSquared(endPoint) < closestDistance) {
254 | closestNode = currentNode;
255 | closestPoint.copy(point);
256 | closestDistance = point.distanceToSquared(endPoint);
257 | }
258 |
259 | const depth = nodeDepth[currentNode.id];
260 | if (depth > 2) continue;
261 |
262 | for (let i = 0; i < currentNode.neighbours.length; i++) {
263 | const neighbour = nodes[currentNode.neighbours[i]];
264 | if (neighbour.id in nodeDepth) continue;
265 |
266 | nodeQueue.push(neighbour);
267 | nodeDepth[neighbour.id] = depth + 1;
268 | }
269 | }
270 |
271 | endTarget.copy(closestPoint);
272 | return closestNode;
273 | };
274 | }());
275 |
276 | /**
277 | * Defines a zone of interconnected groups on a navigation mesh.
278 | *
279 | * @type {Object}
280 | * @property {Array} groups
281 | * @property {Array} vertices
282 | */
283 | const Zone = {}; // jshint ignore:line
284 |
285 | /**
286 | * Defines a group within a navigation mesh.
287 | *
288 | * @type {Object}
289 | */
290 | const Group = {}; // jshint ignore:line
291 |
292 | /**
293 | * Defines a node (or polygon) within a group.
294 | *
295 | * @type {Object}
296 | * @property {number} id
297 | * @property {Array} neighbours IDs of neighboring nodes.
298 | * @property {Array} vertexIds
299 | * @property {Vector3} centroid
300 | * @property {Array>} portals Array of portals, each defined by two vertex IDs.
301 | * @property {boolean} closed
302 | * @property {number} cost
303 | */
304 | const Node = {}; // jshint ignore:line
305 |
306 | export { Pathfinding };
307 |
--------------------------------------------------------------------------------
/projects/shootout/pathfinding/PathfindingHelper.js:
--------------------------------------------------------------------------------
1 | import {
2 | BoxBufferGeometry,
3 | BufferAttribute,
4 | BufferGeometry,
5 | Color,
6 | Line,
7 | LineBasicMaterial,
8 | Mesh,
9 | MeshBasicMaterial,
10 | Object3D,
11 | SphereBufferGeometry,
12 | Vector3,
13 | } from '../three128/three.module.js';
14 |
15 | const colors = {
16 | PLAYER: new Color( 0xee836f ).convertGammaToLinear( 2.2 ).getHex(),
17 | TARGET: new Color( 0xdccb18 ).convertGammaToLinear( 2.2 ).getHex(),
18 | PATH: new Color( 0x00a3af ).convertGammaToLinear( 2.2 ).getHex(),
19 | WAYPOINT: new Color( 0x00a3af ).convertGammaToLinear( 2.2 ).getHex(),
20 | CLAMPED_STEP: new Color( 0xdcd3b2 ).convertGammaToLinear( 2.2 ).getHex(),
21 | CLOSEST_NODE: new Color( 0x43676b ).convertGammaToLinear( 2.2 ).getHex(),
22 | };
23 |
24 | const OFFSET = 0.2;
25 |
26 | /**
27 | * Helper for debugging pathfinding behavior.
28 | */
29 | class PathfindingHelper extends Object3D {
30 | constructor () {
31 | super();
32 |
33 | this._playerMarker = new Mesh(
34 | new SphereBufferGeometry( 0.25, 32, 32 ),
35 | new MeshBasicMaterial( { color: colors.PLAYER } )
36 | );
37 |
38 | this._targetMarker = new Mesh(
39 | new BoxBufferGeometry( 0.3, 0.3, 0.3 ),
40 | new MeshBasicMaterial( { color: colors.TARGET } )
41 | );
42 |
43 |
44 | this._nodeMarker = new Mesh(
45 | new BoxBufferGeometry( 0.1, 0.8, 0.1 ),
46 | new MeshBasicMaterial( { color: colors.CLOSEST_NODE } )
47 | );
48 |
49 |
50 | this._stepMarker = new Mesh(
51 | new BoxBufferGeometry( 0.1, 1, 0.1 ),
52 | new MeshBasicMaterial( { color: colors.CLAMPED_STEP } )
53 | );
54 |
55 | this._pathMarker = new Object3D();
56 |
57 | this._pathLineMaterial = new LineBasicMaterial( { color: colors.PATH, linewidth: 2 } ) ;
58 | this._pathPointMaterial = new MeshBasicMaterial( { color: colors.WAYPOINT } );
59 | this._pathPointGeometry = new SphereBufferGeometry( 0.08 );
60 |
61 | this._markers = [
62 | this._playerMarker,
63 | this._targetMarker,
64 | this._nodeMarker,
65 | this._stepMarker,
66 | this._pathMarker,
67 | ];
68 |
69 | this._markers.forEach( ( marker ) => {
70 |
71 | marker.visible = false;
72 |
73 | this.add( marker );
74 |
75 | } );
76 |
77 | }
78 |
79 | /**
80 | * @param {Array} path
81 | * @return {this}
82 | */
83 | setPath ( path ) {
84 |
85 | while ( this._pathMarker.children.length ) {
86 |
87 | this._pathMarker.children[ 0 ].visible = false;
88 | this._pathMarker.remove( this._pathMarker.children[ 0 ] );
89 |
90 | }
91 |
92 | path = [ this._playerMarker.position ].concat( path );
93 |
94 | // Draw debug lines
95 | const geometry = new BufferGeometry();
96 | geometry.setAttribute('position', new BufferAttribute(new Float32Array(path.length * 3), 3));
97 | for (let i = 0; i < path.length; i++) {
98 | geometry.attributes.position.setXYZ(i, path[ i ].x, path[ i ].y + OFFSET, path[ i ].z);
99 | }
100 | this._pathMarker.add( new Line( geometry, this._pathLineMaterial ) );
101 |
102 | for ( let i = 0; i < path.length - 1; i++ ) {
103 |
104 | const node = new Mesh( this._pathPointGeometry, this._pathPointMaterial );
105 | node.position.copy( path[ i ] );
106 | node.position.y += OFFSET;
107 | this._pathMarker.add( node );
108 |
109 | }
110 |
111 | this._pathMarker.visible = true;
112 |
113 | return this;
114 |
115 | }
116 |
117 | /**
118 | * @param {Vector3} position
119 | * @return {this}
120 | */
121 | setPlayerPosition( position ) {
122 |
123 | this._playerMarker.position.copy( position );
124 | this._playerMarker.visible = true;
125 | return this;
126 |
127 | }
128 |
129 | /**
130 | * @param {Vector3} position
131 | * @return {this}
132 | */
133 | setTargetPosition( position ) {
134 |
135 | this._targetMarker.position.copy( position );
136 | this._targetMarker.visible = true;
137 | return this;
138 |
139 | }
140 |
141 | /**
142 | * @param {Vector3} position
143 | * @return {this}
144 | */
145 | setNodePosition( position ) {
146 |
147 | this._nodeMarker.position.copy( position );
148 | this._nodeMarker.visible = true;
149 | return this;
150 |
151 | }
152 |
153 | /**
154 | * @param {Vector3} position
155 | * @return {this}
156 | */
157 | setStepPosition( position ) {
158 |
159 | this._stepMarker.position.copy( position );
160 | this._stepMarker.visible = true;
161 | return this;
162 |
163 | }
164 |
165 | /**
166 | * Hides all markers.
167 | * @return {this}
168 | */
169 | reset () {
170 |
171 | while ( this._pathMarker.children.length ) {
172 |
173 | this._pathMarker.children[ 0 ].visible = false;
174 | this._pathMarker.remove( this._pathMarker.children[ 0 ] );
175 |
176 | }
177 |
178 | this._markers.forEach( ( marker ) => {
179 |
180 | marker.visible = false;
181 |
182 | } );
183 |
184 | return this;
185 |
186 | }
187 |
188 | }
189 |
190 | export { PathfindingHelper };
191 |
--------------------------------------------------------------------------------
/projects/shootout/pathfinding/Utils.js:
--------------------------------------------------------------------------------
1 | import { BufferAttribute, BufferGeometry, Float32BufferAttribute } from '../three128/three.module.js';
2 |
3 | class Utils {
4 |
5 | static roundNumber (value, decimals) {
6 | const factor = Math.pow(10, decimals);
7 | return Math.round(value * factor) / factor;
8 | }
9 |
10 | static sample (list) {
11 | return list[Math.floor(Math.random() * list.length)];
12 | }
13 |
14 | static distanceToSquared (a, b) {
15 |
16 | var dx = a.x - b.x;
17 | var dy = a.y - b.y;
18 | var dz = a.z - b.z;
19 |
20 | return dx * dx + dy * dy + dz * dz;
21 |
22 | }
23 |
24 | //+ Jonas Raoni Soares Silva
25 | //@ http://jsfromhell.com/math/is-point-in-poly [rev. #0]
26 | static isPointInPoly (poly, pt) {
27 | for (var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
28 | ((poly[i].z <= pt.z && pt.z < poly[j].z) || (poly[j].z <= pt.z && pt.z < poly[i].z)) && (pt.x < (poly[j].x - poly[i].x) * (pt.z - poly[i].z) / (poly[j].z - poly[i].z) + poly[i].x) && (c = !c);
29 | return c;
30 | }
31 |
32 | static isVectorInPolygon (vector, polygon, vertices) {
33 |
34 | // reference point will be the centroid of the polygon
35 | // We need to rotate the vector as well as all the points which the polygon uses
36 |
37 | var lowestPoint = 100000;
38 | var highestPoint = -100000;
39 |
40 | var polygonVertices = [];
41 |
42 | polygon.vertexIds.forEach((vId) => {
43 | lowestPoint = Math.min(vertices[vId].y, lowestPoint);
44 | highestPoint = Math.max(vertices[vId].y, highestPoint);
45 | polygonVertices.push(vertices[vId]);
46 | });
47 |
48 | if (vector.y < highestPoint + 0.5 && vector.y > lowestPoint - 0.5 &&
49 | this.isPointInPoly(polygonVertices, vector)) {
50 | return true;
51 | }
52 | return false;
53 | }
54 |
55 | static triarea2 (a, b, c) {
56 | var ax = b.x - a.x;
57 | var az = b.z - a.z;
58 | var bx = c.x - a.x;
59 | var bz = c.z - a.z;
60 | return bx * az - ax * bz;
61 | }
62 |
63 | static vequal (a, b) {
64 | return this.distanceToSquared(a, b) < 0.00001;
65 | }
66 |
67 | static prepGeometry( geometry, tolerance = 1e-4 ){
68 | tolerance = Math.max( tolerance, Number.EPSILON );
69 | tolerance *= tolerance;
70 |
71 | // Generate an index buffer if the geometry doesn't have one, or optimize it
72 | // if it's already available.
73 | const indices = geometry.getIndex();
74 | const positions = geometry.getAttribute( 'position' );
75 | let vertexCount = (indices) ? indices.count : positions.count;
76 |
77 | const newVertices = [];
78 | const newIndices = [];
79 |
80 | for ( let i = 0; i < vertexCount; i ++ ) {
81 | const index = (indices) ? indices.getX(i) : i;
82 | const pos = { x:positions.getX(index), y:positions.getY(index), z:positions.getZ(index), index:newVertices.length};
83 |
84 | if (!newVertices.some( p => {
85 | if (Utils.distanceToSquared(p, pos) {
98 | vertices.push(p.x);
99 | vertices.push(p.y);
100 | vertices.push(p.z);
101 | });
102 |
103 | const result = new BufferGeometry();
104 |
105 | result.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
106 | result.setIndex( newIndices );
107 |
108 | return result;
109 | }
110 |
111 | /**
112 | * Copied from BufferGeometryUtils.mergeVertices, because importing ES modules
113 | * from a sub-directory makes Node.js (as of v14) angry.
114 | *
115 | * @param {THREE.BufferGeometry} geometry
116 | * @param {number} tolerance
117 | * @return {THREE.BufferGeometry>}
118 | */
119 | static mergeVertices (geometry, tolerance = 1e-4) {
120 |
121 | tolerance = Math.max( tolerance, Number.EPSILON );
122 |
123 | // Generate an index buffer if the geometry doesn't have one, or optimize it
124 | // if it's already available.
125 | var hashToIndex = {};
126 | var indices = geometry.getIndex();
127 | var positions = geometry.getAttribute( 'position' );
128 | var vertexCount = indices ? indices.count : positions.count;
129 |
130 | // next value for triangle indices
131 | var nextIndex = 0;
132 |
133 | // attributes and new attribute arrays
134 | var attributeNames = Object.keys( geometry.attributes );
135 | var attrArrays = {};
136 | var morphAttrsArrays = {};
137 | var newIndices = [];
138 | var getters = [ 'getX', 'getY', 'getZ', 'getW' ];
139 |
140 | // initialize the arrays
141 | for ( var i = 0, l = attributeNames.length; i < l; i ++ ) {
142 |
143 | var name = attributeNames[ i ];
144 |
145 | attrArrays[ name ] = [];
146 |
147 | var morphAttr = geometry.morphAttributes[ name ];
148 | if ( morphAttr ) {
149 |
150 | morphAttrsArrays[ name ] = new Array( morphAttr.length ).fill().map( () => [] );
151 |
152 | }
153 |
154 | }
155 |
156 | // convert the error tolerance to an amount of decimal places to truncate to
157 | var decimalShift = Math.log10( 1 / tolerance );
158 | var shiftMultiplier = Math.pow( 10, decimalShift );
159 | for ( var i = 0; i < vertexCount; i ++ ) {
160 |
161 | var index = indices ? indices.getX( i ) : i;
162 |
163 | // Generate a hash for the vertex attributes at the current index 'i'
164 | var hash = '';
165 | for ( var j = 0, l = attributeNames.length; j < l; j ++ ) {
166 |
167 | var name = attributeNames[ j ];
168 | var attribute = geometry.getAttribute( name );
169 | var itemSize = attribute.itemSize;
170 |
171 | for ( var k = 0; k < itemSize; k ++ ) {
172 |
173 | // double tilde truncates the decimal value
174 | hash += `${ ~ ~ ( attribute[ getters[ k ] ]( index ) * shiftMultiplier ) },`;
175 |
176 | }
177 |
178 | }
179 |
180 | // Add another reference to the vertex if it's already
181 | // used by another index
182 | if ( hash in hashToIndex ) {
183 |
184 | newIndices.push( hashToIndex[ hash ] );
185 |
186 | } else {
187 |
188 | // copy data to the new index in the attribute arrays
189 | for ( var j = 0, l = attributeNames.length; j < l; j ++ ) {
190 |
191 | var name = attributeNames[ j ];
192 | var attribute = geometry.getAttribute( name );
193 | var morphAttr = geometry.morphAttributes[ name ];
194 | var itemSize = attribute.itemSize;
195 | var newarray = attrArrays[ name ];
196 | var newMorphArrays = morphAttrsArrays[ name ];
197 |
198 | for ( var k = 0; k < itemSize; k ++ ) {
199 |
200 | var getterFunc = getters[ k ];
201 | newarray.push( attribute[ getterFunc ]( index ) );
202 |
203 | if ( morphAttr ) {
204 |
205 | for ( var m = 0, ml = morphAttr.length; m < ml; m ++ ) {
206 |
207 | newMorphArrays[ m ].push( morphAttr[ m ][ getterFunc ]( index ) );
208 |
209 | }
210 |
211 | }
212 |
213 | }
214 |
215 | }
216 |
217 | hashToIndex[ hash ] = nextIndex;
218 | newIndices.push( nextIndex );
219 | nextIndex ++;
220 |
221 | }
222 |
223 | }
224 |
225 | // Generate typed arrays from new attribute arrays and update
226 | // the attributeBuffers
227 | const result = geometry.clone();
228 | for ( var i = 0, l = attributeNames.length; i < l; i ++ ) {
229 |
230 | var name = attributeNames[ i ];
231 | var oldAttribute = geometry.getAttribute( name );
232 |
233 | var buffer = new oldAttribute.array.constructor( attrArrays[ name ] );
234 | var attribute = new BufferAttribute( buffer, oldAttribute.itemSize, oldAttribute.normalized );
235 |
236 | result.setAttribute( name, attribute );
237 |
238 | // Update the attribute arrays
239 | if ( name in morphAttrsArrays ) {
240 |
241 | for ( var j = 0; j < morphAttrsArrays[ name ].length; j ++ ) {
242 |
243 | var oldMorphAttribute = geometry.morphAttributes[ name ][ j ];
244 |
245 | var buffer = new oldMorphAttribute.array.constructor( morphAttrsArrays[ name ][ j ] );
246 | var morphAttribute = new BufferAttribute( buffer, oldMorphAttribute.itemSize, oldMorphAttribute.normalized );
247 | result.morphAttributes[ name ][ j ] = morphAttribute;
248 |
249 | }
250 |
251 | }
252 |
253 | }
254 |
255 | // indices
256 |
257 | result.setIndex( newIndices );
258 |
259 | return result;
260 |
261 | }
262 | }
263 |
264 | export { Utils };
265 |
--------------------------------------------------------------------------------
/projects/shootout/pathfinding/index.js:
--------------------------------------------------------------------------------
1 | import { Pathfinding } from './Pathfinding.js';
2 | import { PathfindingHelper } from './PathfindingHelper.js';
3 |
4 | export { Pathfinding, PathfindingHelper };
5 |
--------------------------------------------------------------------------------
/projects/shootout/three128/pp/CopyShader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Full-screen textured quad shader
3 | */
4 |
5 | var CopyShader = {
6 |
7 | uniforms: {
8 |
9 | 'tDiffuse': { value: null },
10 | 'opacity': { value: 1.0 }
11 |
12 | },
13 |
14 | vertexShader: /* glsl */`
15 |
16 | varying vec2 vUv;
17 |
18 | void main() {
19 |
20 | vUv = uv;
21 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
22 |
23 | }`,
24 |
25 | fragmentShader: /* glsl */`
26 |
27 | uniform float opacity;
28 |
29 | uniform sampler2D tDiffuse;
30 |
31 | varying vec2 vUv;
32 |
33 | void main() {
34 |
35 | vec4 texel = texture2D( tDiffuse, vUv );
36 | gl_FragColor = opacity * texel;
37 |
38 | }`
39 |
40 | };
41 |
42 | export { CopyShader };
43 |
--------------------------------------------------------------------------------
/projects/shootout/three128/pp/EffectComposer.js:
--------------------------------------------------------------------------------
1 | import {
2 | BufferGeometry,
3 | Clock,
4 | Float32BufferAttribute,
5 | LinearFilter,
6 | Mesh,
7 | OrthographicCamera,
8 | RGBAFormat,
9 | Vector2,
10 | WebGLRenderTarget
11 | } from '../three.module.js';
12 | import { CopyShader } from './CopyShader.js';
13 | import { ShaderPass } from './ShaderPass.js';
14 | import { MaskPass } from './MaskPass.js';
15 | import { ClearMaskPass } from './MaskPass.js';
16 |
17 | class EffectComposer {
18 |
19 | constructor( renderer, renderTarget ) {
20 |
21 | this.renderer = renderer;
22 |
23 | if ( renderTarget === undefined ) {
24 |
25 | const parameters = {
26 | minFilter: LinearFilter,
27 | magFilter: LinearFilter,
28 | format: RGBAFormat
29 | };
30 |
31 | const size = renderer.getSize( new Vector2() );
32 | this._pixelRatio = renderer.getPixelRatio();
33 | this._width = size.width;
34 | this._height = size.height;
35 |
36 | renderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, parameters );
37 | renderTarget.texture.name = 'EffectComposer.rt1';
38 |
39 | } else {
40 |
41 | this._pixelRatio = 1;
42 | this._width = renderTarget.width;
43 | this._height = renderTarget.height;
44 |
45 | }
46 |
47 | this.renderTarget1 = renderTarget;
48 | this.renderTarget2 = renderTarget.clone();
49 | this.renderTarget2.texture.name = 'EffectComposer.rt2';
50 |
51 | this.writeBuffer = this.renderTarget1;
52 | this.readBuffer = this.renderTarget2;
53 |
54 | this.renderToScreen = true;
55 |
56 | this.passes = [];
57 |
58 | // dependencies
59 |
60 | if ( CopyShader === undefined ) {
61 |
62 | console.error( 'THREE.EffectComposer relies on CopyShader' );
63 |
64 | }
65 |
66 | if ( ShaderPass === undefined ) {
67 |
68 | console.error( 'THREE.EffectComposer relies on ShaderPass' );
69 |
70 | }
71 |
72 | this.copyPass = new ShaderPass( CopyShader );
73 |
74 | this.clock = new Clock();
75 |
76 | }
77 |
78 | swapBuffers() {
79 |
80 | const tmp = this.readBuffer;
81 | this.readBuffer = this.writeBuffer;
82 | this.writeBuffer = tmp;
83 |
84 | }
85 |
86 | addPass( pass ) {
87 |
88 | this.passes.push( pass );
89 | pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
90 |
91 | }
92 |
93 | insertPass( pass, index ) {
94 |
95 | this.passes.splice( index, 0, pass );
96 | pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
97 |
98 | }
99 |
100 | removePass( pass ) {
101 |
102 | const index = this.passes.indexOf( pass );
103 |
104 | if ( index !== - 1 ) {
105 |
106 | this.passes.splice( index, 1 );
107 |
108 | }
109 |
110 | }
111 |
112 | isLastEnabledPass( passIndex ) {
113 |
114 | for ( let i = passIndex + 1; i < this.passes.length; i ++ ) {
115 |
116 | if ( this.passes[ i ].enabled ) {
117 |
118 | return false;
119 |
120 | }
121 |
122 | }
123 |
124 | return true;
125 |
126 | }
127 |
128 | render( deltaTime ) {
129 |
130 | // deltaTime value is in seconds
131 |
132 | if ( deltaTime === undefined ) {
133 |
134 | deltaTime = this.clock.getDelta();
135 |
136 | }
137 |
138 | const currentRenderTarget = this.renderer.getRenderTarget();
139 |
140 | let maskActive = false;
141 |
142 | for ( let i = 0, il = this.passes.length; i < il; i ++ ) {
143 |
144 | const pass = this.passes[ i ];
145 |
146 | if ( pass.enabled === false ) continue;
147 |
148 | pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );
149 | pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );
150 |
151 | if ( pass.needsSwap ) {
152 |
153 | if ( maskActive ) {
154 |
155 | const context = this.renderer.getContext();
156 | const stencil = this.renderer.state.buffers.stencil;
157 |
158 | //context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );
159 | stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );
160 |
161 | this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );
162 |
163 | //context.stencilFunc( context.EQUAL, 1, 0xffffffff );
164 | stencil.setFunc( context.EQUAL, 1, 0xffffffff );
165 |
166 | }
167 |
168 | this.swapBuffers();
169 |
170 | }
171 |
172 | if ( MaskPass !== undefined ) {
173 |
174 | if ( pass instanceof MaskPass ) {
175 |
176 | maskActive = true;
177 |
178 | } else if ( pass instanceof ClearMaskPass ) {
179 |
180 | maskActive = false;
181 |
182 | }
183 |
184 | }
185 |
186 | }
187 |
188 | this.renderer.setRenderTarget( currentRenderTarget );
189 |
190 | }
191 |
192 | reset( renderTarget ) {
193 |
194 | if ( renderTarget === undefined ) {
195 |
196 | const size = this.renderer.getSize( new Vector2() );
197 | this._pixelRatio = this.renderer.getPixelRatio();
198 | this._width = size.width;
199 | this._height = size.height;
200 |
201 | renderTarget = this.renderTarget1.clone();
202 | renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
203 |
204 | }
205 |
206 | this.renderTarget1.dispose();
207 | this.renderTarget2.dispose();
208 | this.renderTarget1 = renderTarget;
209 | this.renderTarget2 = renderTarget.clone();
210 |
211 | this.writeBuffer = this.renderTarget1;
212 | this.readBuffer = this.renderTarget2;
213 |
214 | }
215 |
216 | setSize( width, height ) {
217 |
218 | this._width = width;
219 | this._height = height;
220 |
221 | const effectiveWidth = this._width * this._pixelRatio;
222 | const effectiveHeight = this._height * this._pixelRatio;
223 |
224 | this.renderTarget1.setSize( effectiveWidth, effectiveHeight );
225 | this.renderTarget2.setSize( effectiveWidth, effectiveHeight );
226 |
227 | for ( let i = 0; i < this.passes.length; i ++ ) {
228 |
229 | this.passes[ i ].setSize( effectiveWidth, effectiveHeight );
230 |
231 | }
232 |
233 | }
234 |
235 | setPixelRatio( pixelRatio ) {
236 |
237 | this._pixelRatio = pixelRatio;
238 |
239 | this.setSize( this._width, this._height );
240 |
241 | }
242 |
243 | }
244 |
245 |
246 | class Pass {
247 |
248 | constructor() {
249 |
250 | // if set to true, the pass is processed by the composer
251 | this.enabled = true;
252 |
253 | // if set to true, the pass indicates to swap read and write buffer after rendering
254 | this.needsSwap = true;
255 |
256 | // if set to true, the pass clears its buffer before rendering
257 | this.clear = false;
258 |
259 | // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer.
260 | this.renderToScreen = false;
261 |
262 | }
263 |
264 | setSize( /* width, height */ ) {}
265 |
266 | render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
267 |
268 | console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
269 |
270 | }
271 |
272 | }
273 |
274 | // Helper for passes that need to fill the viewport with a single quad.
275 |
276 | const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
277 |
278 | // https://github.com/mrdoob/three.js/pull/21358
279 |
280 | const _geometry = new BufferGeometry();
281 | _geometry.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
282 | _geometry.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );
283 |
284 | class FullScreenQuad {
285 |
286 | constructor( material ) {
287 |
288 | this._mesh = new Mesh( _geometry, material );
289 |
290 | }
291 |
292 | dispose() {
293 |
294 | this._mesh.geometry.dispose();
295 |
296 | }
297 |
298 | render( renderer ) {
299 |
300 | renderer.render( this._mesh, _camera );
301 |
302 | }
303 |
304 | get material() {
305 |
306 | return this._mesh.material;
307 |
308 | }
309 |
310 | set material( value ) {
311 |
312 | this._mesh.material = value;
313 |
314 | }
315 |
316 | }
317 |
318 | export { EffectComposer, Pass, FullScreenQuad };
319 |
--------------------------------------------------------------------------------
/projects/shootout/three128/pp/GammaCorrectionShader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Gamma Correction Shader
3 | * http://en.wikipedia.org/wiki/gamma_correction
4 | */
5 |
6 | const GammaCorrectionShader = {
7 |
8 | uniforms: {
9 |
10 | 'tDiffuse': { value: null }
11 |
12 | },
13 |
14 | vertexShader: /* glsl */`
15 |
16 | varying vec2 vUv;
17 |
18 | void main() {
19 |
20 | vUv = uv;
21 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
22 |
23 | }`,
24 |
25 | fragmentShader: /* glsl */`
26 |
27 | uniform sampler2D tDiffuse;
28 |
29 | varying vec2 vUv;
30 |
31 | void main() {
32 |
33 | vec4 tex = texture2D( tDiffuse, vUv );
34 |
35 | gl_FragColor = LinearTosRGB( tex ); // optional: LinearToGamma( tex, float( GAMMA_FACTOR ) );
36 |
37 | }`
38 |
39 | };
40 |
41 | export { GammaCorrectionShader };
42 |
--------------------------------------------------------------------------------
/projects/shootout/three128/pp/MaskPass.js:
--------------------------------------------------------------------------------
1 | import { Pass } from './Pass.js';
2 |
3 | class MaskPass extends Pass {
4 |
5 | constructor( scene, camera ) {
6 |
7 | super();
8 |
9 | this.scene = scene;
10 | this.camera = camera;
11 |
12 | this.clear = true;
13 | this.needsSwap = false;
14 |
15 | this.inverse = false;
16 |
17 | }
18 |
19 | render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
20 |
21 | const context = renderer.getContext();
22 | const state = renderer.state;
23 |
24 | // don't update color or depth
25 |
26 | state.buffers.color.setMask( false );
27 | state.buffers.depth.setMask( false );
28 |
29 | // lock buffers
30 |
31 | state.buffers.color.setLocked( true );
32 | state.buffers.depth.setLocked( true );
33 |
34 | // set up stencil
35 |
36 | let writeValue, clearValue;
37 |
38 | if ( this.inverse ) {
39 |
40 | writeValue = 0;
41 | clearValue = 1;
42 |
43 | } else {
44 |
45 | writeValue = 1;
46 | clearValue = 0;
47 |
48 | }
49 |
50 | state.buffers.stencil.setTest( true );
51 | state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE );
52 | state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff );
53 | state.buffers.stencil.setClear( clearValue );
54 | state.buffers.stencil.setLocked( true );
55 |
56 | // draw into the stencil buffer
57 |
58 | renderer.setRenderTarget( readBuffer );
59 | if ( this.clear ) renderer.clear();
60 | renderer.render( this.scene, this.camera );
61 |
62 | renderer.setRenderTarget( writeBuffer );
63 | if ( this.clear ) renderer.clear();
64 | renderer.render( this.scene, this.camera );
65 |
66 | // unlock color and depth buffer for subsequent rendering
67 |
68 | state.buffers.color.setLocked( false );
69 | state.buffers.depth.setLocked( false );
70 |
71 | // only render where stencil is set to 1
72 |
73 | state.buffers.stencil.setLocked( false );
74 | state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1
75 | state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP );
76 | state.buffers.stencil.setLocked( true );
77 |
78 | }
79 |
80 | }
81 |
82 | class ClearMaskPass extends Pass {
83 |
84 | constructor() {
85 |
86 | super();
87 |
88 | this.needsSwap = false;
89 |
90 | }
91 |
92 | render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
93 |
94 | renderer.state.buffers.stencil.setLocked( false );
95 | renderer.state.buffers.stencil.setTest( false );
96 |
97 | }
98 |
99 | }
100 |
101 | export { MaskPass, ClearMaskPass };
102 |
--------------------------------------------------------------------------------
/projects/shootout/three128/pp/Pass.js:
--------------------------------------------------------------------------------
1 | import {
2 | BufferGeometry,
3 | Float32BufferAttribute,
4 | OrthographicCamera,
5 | Mesh
6 | } from '../three.module.js';
7 |
8 | class Pass {
9 |
10 | constructor() {
11 |
12 | // if set to true, the pass is processed by the composer
13 | this.enabled = true;
14 |
15 | // if set to true, the pass indicates to swap read and write buffer after rendering
16 | this.needsSwap = true;
17 |
18 | // if set to true, the pass clears its buffer before rendering
19 | this.clear = false;
20 |
21 | // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer.
22 | this.renderToScreen = false;
23 |
24 | }
25 |
26 | setSize( /* width, height */ ) {}
27 |
28 | render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
29 |
30 | console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
31 |
32 | }
33 |
34 | }
35 |
36 | // Helper for passes that need to fill the viewport with a single quad.
37 |
38 | const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
39 |
40 | // https://github.com/mrdoob/three.js/pull/21358
41 |
42 | const _geometry = new BufferGeometry();
43 | _geometry.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
44 | _geometry.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );
45 |
46 | class FullScreenQuad {
47 |
48 | constructor( material ) {
49 |
50 | this._mesh = new Mesh( _geometry, material );
51 |
52 | }
53 |
54 | dispose() {
55 |
56 | this._mesh.geometry.dispose();
57 |
58 | }
59 |
60 | render( renderer ) {
61 |
62 | renderer.render( this._mesh, _camera );
63 |
64 | }
65 |
66 | get material() {
67 |
68 | return this._mesh.material;
69 |
70 | }
71 |
72 | set material( value ) {
73 |
74 | this._mesh.material = value;
75 |
76 | }
77 |
78 | }
79 |
80 | export { Pass, FullScreenQuad };
81 |
--------------------------------------------------------------------------------
/projects/shootout/three128/pp/RenderPass.js:
--------------------------------------------------------------------------------
1 | import {
2 | Color
3 | } from '../three.module.js';
4 | import { Pass } from './Pass.js';
5 |
6 | class RenderPass extends Pass {
7 |
8 | constructor( scene, camera, overrideMaterial, clearColor, clearAlpha ) {
9 |
10 | super();
11 |
12 | this.scene = scene;
13 | this.camera = camera;
14 |
15 | this.overrideMaterial = overrideMaterial;
16 |
17 | this.clearColor = clearColor;
18 | this.clearAlpha = ( clearAlpha !== undefined ) ? clearAlpha : 0;
19 |
20 | this.clear = true;
21 | this.clearDepth = false;
22 | this.needsSwap = false;
23 | this._oldClearColor = new Color();
24 |
25 | }
26 |
27 | render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
28 |
29 | const oldAutoClear = renderer.autoClear;
30 | renderer.autoClear = false;
31 |
32 | let oldClearAlpha, oldOverrideMaterial;
33 |
34 | if ( this.overrideMaterial !== undefined ) {
35 |
36 | oldOverrideMaterial = this.scene.overrideMaterial;
37 |
38 | this.scene.overrideMaterial = this.overrideMaterial;
39 |
40 | }
41 |
42 | if ( this.clearColor ) {
43 |
44 | renderer.getClearColor( this._oldClearColor );
45 | oldClearAlpha = renderer.getClearAlpha();
46 |
47 | renderer.setClearColor( this.clearColor, this.clearAlpha );
48 |
49 | }
50 |
51 | if ( this.clearDepth ) {
52 |
53 | renderer.clearDepth();
54 |
55 | }
56 |
57 | renderer.setRenderTarget( this.renderToScreen ? null : readBuffer );
58 |
59 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
60 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
61 | renderer.render( this.scene, this.camera );
62 |
63 | if ( this.clearColor ) {
64 |
65 | renderer.setClearColor( this._oldClearColor, oldClearAlpha );
66 |
67 | }
68 |
69 | if ( this.overrideMaterial !== undefined ) {
70 |
71 | this.scene.overrideMaterial = oldOverrideMaterial;
72 |
73 | }
74 |
75 | renderer.autoClear = oldAutoClear;
76 |
77 | }
78 |
79 | }
80 |
81 | export { RenderPass };
82 |
--------------------------------------------------------------------------------
/projects/shootout/three128/pp/ShaderPass.js:
--------------------------------------------------------------------------------
1 | import {
2 | ShaderMaterial,
3 | UniformsUtils
4 | } from '../three.module.js';
5 | import { Pass, FullScreenQuad } from './Pass.js';
6 |
7 | class ShaderPass extends Pass {
8 |
9 | constructor( shader, textureID ) {
10 |
11 | super();
12 |
13 | this.textureID = ( textureID !== undefined ) ? textureID : 'tDiffuse';
14 |
15 | if ( shader instanceof ShaderMaterial ) {
16 |
17 | this.uniforms = shader.uniforms;
18 |
19 | this.material = shader;
20 |
21 | } else if ( shader ) {
22 |
23 | this.uniforms = UniformsUtils.clone( shader.uniforms );
24 |
25 | this.material = new ShaderMaterial( {
26 |
27 | defines: Object.assign( {}, shader.defines ),
28 | uniforms: this.uniforms,
29 | vertexShader: shader.vertexShader,
30 | fragmentShader: shader.fragmentShader
31 |
32 | } );
33 |
34 | }
35 |
36 | this.fsQuad = new FullScreenQuad( this.material );
37 |
38 | }
39 |
40 | render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
41 |
42 | if ( this.uniforms[ this.textureID ] ) {
43 |
44 | this.uniforms[ this.textureID ].value = readBuffer.texture;
45 |
46 | }
47 |
48 | this.fsQuad.material = this.material;
49 |
50 | if ( this.renderToScreen ) {
51 |
52 | renderer.setRenderTarget( null );
53 | this.fsQuad.render( renderer );
54 |
55 | } else {
56 |
57 | renderer.setRenderTarget( writeBuffer );
58 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
59 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
60 | this.fsQuad.render( renderer );
61 |
62 | }
63 |
64 | }
65 |
66 | }
67 |
68 | export { ShaderPass };
69 |
--------------------------------------------------------------------------------
/projects/sphere/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Sphere
8 |
9 |
10 |
11 |
Feel the Sphere
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/projects/sphere/main.js:
--------------------------------------------------------------------------------
1 | import './style.css';
2 | import * as THREE from 'three';
3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
4 | import * as dat from 'dat.gui';
5 |
6 | // Loading
7 | const textureLoader = new THREE.TextureLoader();
8 | const normalTexture = textureLoader.load('/assets/textures/NormalMap.png');
9 |
10 | // Debug
11 | const gui = new dat.GUI();
12 |
13 | // Canvas
14 | const canvas = document.querySelector('canvas.webgl');
15 |
16 | // Scene
17 | const scene = new THREE.Scene();
18 |
19 | // Objects
20 | const geometry = new THREE.SphereBufferGeometry(0.5, 64, 64);
21 |
22 | // Materials
23 | const material = new THREE.MeshStandardMaterial();
24 | material.metalness = 0.7;
25 | material.roughness = 0.2;
26 | material.normalMap = normalTexture;
27 | material.color = new THREE.Color(0x292929);
28 |
29 | // Mesh
30 | const sphere = new THREE.Mesh(geometry, material);
31 | scene.add(sphere);
32 |
33 | // Lights
34 |
35 | const pointLight = new THREE.PointLight(0xffffff, 0.1);
36 | pointLight.position.x = 2;
37 | pointLight.position.y = 3;
38 | pointLight.position.z = 4;
39 | scene.add(pointLight);
40 |
41 | const pointLight2 = new THREE.PointLight(0x0e1f0, 2);
42 | pointLight2.position.set(-1.86, 1.33, -0.66);
43 | pointLight2.intensity = 7;
44 | scene.add(pointLight2);
45 |
46 | const pointLight3 = new THREE.PointLight(0xe11212, 0.1);
47 | pointLight3.position.set(2.13, -1.31, -0.92);
48 | pointLight3.intensity = 4.28;
49 | scene.add(pointLight3);
50 |
51 | /**
52 | * Sizes
53 | */
54 | const sizes = {
55 | width: window.innerWidth,
56 | height: window.innerHeight,
57 | };
58 |
59 | window.addEventListener('resize', () => {
60 | // Update sizes
61 | sizes.width = window.innerWidth;
62 | sizes.height = window.innerHeight;
63 |
64 | // Update camera
65 | camera.aspect = sizes.width / sizes.height;
66 | camera.updateProjectionMatrix();
67 |
68 | // Update renderer
69 | renderer.setSize(sizes.width, sizes.height);
70 | renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
71 | });
72 |
73 | /**
74 | * Camera
75 | */
76 | // Base camera
77 | const camera = new THREE.PerspectiveCamera(
78 | 75,
79 | sizes.width / sizes.height,
80 | 0.1,
81 | 100
82 | );
83 | camera.position.x = 0;
84 | camera.position.y = 0;
85 | camera.position.z = 2;
86 |
87 | scene.add(camera);
88 | // const light2 = gui.addFolder('Light 2');
89 | // light2.add(pointLight2.position, 'x').min(-3).max(3).step(0.01);
90 | // light2.add(pointLight2.position, 'y').min(-6).max(6).step(0.01);
91 | // light2.add(pointLight2.position, 'z').min(-3).max(3).step(0.01);
92 | // light2.add(pointLight2, 'intensity').min(0).max(10).step(0.01);
93 | // const light3 = gui.addFolder('Light 3');
94 | // light3.add(pointLight3.position, 'x').min(-3).max(3).step(0.01);
95 | // light3.add(pointLight3.position, 'y').min(-6).max(6).step(0.01);
96 | // light3.add(pointLight3.position, 'z').min(-3).max(3).step(0.01);
97 | // light3.add(pointLight3, 'intensity').min(0).max(20).step(0.01);
98 |
99 | // const light3Color = {
100 | // color: 0x0000ff,
101 | // };
102 | // light2
103 | // .addColor(light3Color, 'color')
104 | // .onChange(() => pointLight2.color.set(light3Color.color));
105 | // light3
106 | // .addColor(light3Color, 'color')
107 | // .onChange(() => pointLight3.color.set(light3Color.color));
108 |
109 | // const pointLight2Helper = new THREE.PointLightHelper(pointLight2, 1);
110 | // scene.add(pointLight2Helper);
111 | // const pointLight3Helper = new THREE.PointLightHelper(pointLight3, 1);
112 | // scene.add(pointLight3Helper);
113 |
114 | // Controls
115 | // const controls = new OrbitControls(camera, canvas)
116 | // controls.enableDamping = true
117 |
118 | /**
119 | * Renderer
120 | */
121 | const renderer = new THREE.WebGLRenderer({
122 | canvas: canvas,
123 | alpha: true,
124 | });
125 | renderer.setSize(sizes.width, sizes.height);
126 | renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
127 |
128 | /**
129 | * Animate
130 | */
131 |
132 | document.addEventListener('mousemove', onDocumentMouseMove);
133 | let mouseX = 0;
134 | let mouseY = 0;
135 |
136 | let targetX = 0;
137 | let targetY = 0;
138 |
139 | const windowHalfX = window.innerWidth / 2;
140 | const windowHalfY = window.innerHeight / 2;
141 |
142 | function onDocumentMouseMove(event) {
143 | mouseX = event.clientX - windowHalfX;
144 | mouseY = event.clientY - windowHalfY;
145 | }
146 |
147 | document.addEventListener('scroll', updateSphere);
148 | function updateSphere(event) {
149 | sphere.position.y = window.scrollY * 0.005;
150 | }
151 |
152 | const clock = new THREE.Clock();
153 |
154 | const tick = () => {
155 | targetX = mouseX * 0.001;
156 | targetY = mouseY * 0.001;
157 |
158 | const elapsedTime = clock.getElapsedTime();
159 |
160 | // Update objects
161 | sphere.rotation.y = 0.5 * elapsedTime;
162 |
163 | sphere.rotation.y += 0.5 * (targetX - sphere.rotation.y);
164 | sphere.rotation.x += 0.5 * (targetY - sphere.rotation.x);
165 | sphere.position.z += 0.8 * (targetY - sphere.rotation.x);
166 |
167 | // Update Orbital Controls
168 | // controls.update()
169 |
170 | // Render
171 | renderer.render(scene, camera);
172 |
173 | // Call tick again on the next frame
174 | window.requestAnimationFrame(tick);
175 | };
176 |
177 | tick();
178 |
--------------------------------------------------------------------------------
/projects/sphere/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | html,
7 | body {
8 | height: 100vh;
9 | font-family: 'Circular-Loom';
10 | background: rgb(24, 24, 24);
11 | }
12 |
13 | body {
14 | overflow-x: hidden;
15 | }
16 |
17 | .webgl {
18 | position: absolute;
19 | top: 0;
20 | left: 0;
21 | outline: none;
22 | mix-blend-mode: exclusion;
23 | }
24 |
25 | .container {
26 | height: 100vh;
27 | display: grid;
28 | place-items: center;
29 | }
30 |
31 | h1 {
32 | font-size: 8rem;
33 | text-transform: uppercase;
34 | color: white;
35 | }
36 |
37 | section {
38 | height: 100vh;
39 | }
40 |
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | https://rlamba.com/ https://rishablamba.com/ 301!
--------------------------------------------------------------------------------
/public/assets/draco/README.md:
--------------------------------------------------------------------------------
1 | # Draco 3D Data Compression
2 |
3 | Draco is an open-source library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics.
4 |
5 | [Website](https://google.github.io/draco/) | [GitHub](https://github.com/google/draco)
6 |
7 | ## Contents
8 |
9 | This folder contains three utilities:
10 |
11 | * `draco_decoder.js` — Emscripten-compiled decoder, compatible with any modern browser.
12 | * `draco_decoder.wasm` — WebAssembly decoder, compatible with newer browsers and devices.
13 | * `draco_wasm_wrapper.js` — JavaScript wrapper for the WASM decoder.
14 |
15 | Each file is provided in two variations:
16 |
17 | * **Default:** Latest stable builds, tracking the project's [master branch](https://github.com/google/draco).
18 | * **glTF:** Builds targeted by the [glTF mesh compression extension](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression), tracking the [corresponding Draco branch](https://github.com/google/draco/tree/gltf_2.0_draco_extension).
19 |
20 | Either variation may be used with `THREE.DRACOLoader`:
21 |
22 | ```js
23 | var dracoLoader = new THREE.DRACOLoader();
24 | dracoLoader.setDecoderPath('path/to/decoders/');
25 | dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support.
26 | ```
27 |
28 | Further [documentation on GitHub](https://github.com/google/draco/tree/master/javascript/example#static-loading-javascript-decoder).
29 |
30 | ## License
31 |
32 | [Apache License 2.0](https://github.com/google/draco/blob/master/LICENSE)
33 |
--------------------------------------------------------------------------------
/public/assets/draco/draco_decoder.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/draco/draco_decoder.wasm
--------------------------------------------------------------------------------
/public/assets/draco/gltf/draco_decoder.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/draco/gltf/draco_decoder.wasm
--------------------------------------------------------------------------------
/public/assets/factory/ammo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
17 |
18 |
19 |
20 |
21 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/public/assets/factory/eve-rifle.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/eve-rifle.glb
--------------------------------------------------------------------------------
/public/assets/factory/eve.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/eve.glb
--------------------------------------------------------------------------------
/public/assets/factory/eve2.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/eve2.glb
--------------------------------------------------------------------------------
/public/assets/factory/factory1.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/factory1.glb
--------------------------------------------------------------------------------
/public/assets/factory/factory2.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/factory2.glb
--------------------------------------------------------------------------------
/public/assets/factory/gameover.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/gameover.ai
--------------------------------------------------------------------------------
/public/assets/factory/gameover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/gameover.png
--------------------------------------------------------------------------------
/public/assets/factory/health.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/public/assets/factory/playagain.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/playagain.ai
--------------------------------------------------------------------------------
/public/assets/factory/playagain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/playagain.png
--------------------------------------------------------------------------------
/public/assets/factory/playgame.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/playgame.ai
--------------------------------------------------------------------------------
/public/assets/factory/playgame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/playgame.png
--------------------------------------------------------------------------------
/public/assets/factory/sfx/atmos.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/sfx/atmos.mp3
--------------------------------------------------------------------------------
/public/assets/factory/sfx/eve-groan.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/sfx/eve-groan.mp3
--------------------------------------------------------------------------------
/public/assets/factory/sfx/footsteps.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/sfx/footsteps.mp3
--------------------------------------------------------------------------------
/public/assets/factory/sfx/groan.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/sfx/groan.mp3
--------------------------------------------------------------------------------
/public/assets/factory/sfx/shot.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/sfx/shot.mp3
--------------------------------------------------------------------------------
/public/assets/factory/sniper-rifle.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/sniper-rifle.glb
--------------------------------------------------------------------------------
/public/assets/factory/swat-guy-rifle.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/swat-guy-rifle.glb
--------------------------------------------------------------------------------
/public/assets/factory/swat-guy.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/swat-guy.glb
--------------------------------------------------------------------------------
/public/assets/factory/swat-guy2.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/factory/swat-guy2.glb
--------------------------------------------------------------------------------
/public/assets/hdr/apartment.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/hdr/apartment.hdr
--------------------------------------------------------------------------------
/public/assets/hdr/factory.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/hdr/factory.hdr
--------------------------------------------------------------------------------
/public/assets/hdr/field_sky.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/hdr/field_sky.hdr
--------------------------------------------------------------------------------
/public/assets/hdr/living_room.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/hdr/living_room.hdr
--------------------------------------------------------------------------------
/public/assets/hdr/venice_sunset_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/hdr/venice_sunset_1k.hdr
--------------------------------------------------------------------------------
/public/assets/plane/bomb.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/bomb.glb
--------------------------------------------------------------------------------
/public/assets/plane/bonus.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/bonus.mp3
--------------------------------------------------------------------------------
/public/assets/plane/engine.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/engine.mp3
--------------------------------------------------------------------------------
/public/assets/plane/explosion.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/explosion.mp3
--------------------------------------------------------------------------------
/public/assets/plane/explosion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/explosion.png
--------------------------------------------------------------------------------
/public/assets/plane/gameover.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/gameover.mp3
--------------------------------------------------------------------------------
/public/assets/plane/gliss.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/gliss.mp3
--------------------------------------------------------------------------------
/public/assets/plane/microplane.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/microplane.glb
--------------------------------------------------------------------------------
/public/assets/plane/paintedsky/nx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/paintedsky/nx.jpg
--------------------------------------------------------------------------------
/public/assets/plane/paintedsky/ny.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/paintedsky/ny.jpg
--------------------------------------------------------------------------------
/public/assets/plane/paintedsky/nz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/paintedsky/nz.jpg
--------------------------------------------------------------------------------
/public/assets/plane/paintedsky/px.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/paintedsky/px.jpg
--------------------------------------------------------------------------------
/public/assets/plane/paintedsky/py.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/paintedsky/py.jpg
--------------------------------------------------------------------------------
/public/assets/plane/paintedsky/pz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/paintedsky/pz.jpg
--------------------------------------------------------------------------------
/public/assets/plane/plane-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/plane-icon.png
--------------------------------------------------------------------------------
/public/assets/plane/star-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/star-icon.png
--------------------------------------------------------------------------------
/public/assets/plane/star.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/plane/star.glb
--------------------------------------------------------------------------------
/public/assets/pool-table/10ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/10ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/11ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/11ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/12ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/12ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/13ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/13ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/14ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/14ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/15ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/15ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/1ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/1ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/2ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/2ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/3ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/3ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/4ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/4ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/5ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/5ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/6ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/6ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/7ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/7ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/8ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/8ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/9ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/9ball.png
--------------------------------------------------------------------------------
/public/assets/pool-table/pool-table.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/pool-table/pool-table.glb
--------------------------------------------------------------------------------
/public/assets/star-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/assets/star-icon.png
--------------------------------------------------------------------------------
/public/img/denim_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/img/denim_.jpg
--------------------------------------------------------------------------------
/public/img/fabric_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/img/fabric_.jpg
--------------------------------------------------------------------------------
/public/img/pattern_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/img/pattern_.jpg
--------------------------------------------------------------------------------
/public/img/quilt_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/img/quilt_.jpg
--------------------------------------------------------------------------------
/public/img/wood_.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/public/img/wood_.jpg
--------------------------------------------------------------------------------
/sphere.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rikki407/threejs-projects/08a50682c57ed7c6a909f041e63bac7c34c171b5/sphere.gif
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | .content-wrapper {
8 | position: absolute;
9 | top: 50%;
10 | left: 50%;
11 | transform: translate(-50%, -50%);
12 | text-align: center;
13 | color: #ffffff;
14 | }
15 | h1 {
16 | font-family: 'Space Mono', monospace;
17 | letter-spacing: 0.025em;
18 | text-transform: uppercase;
19 | font-size: 0.875rem;
20 | line-height: 1.25rem;
21 | }
22 |
23 | p {
24 | font-size: 2.25rem;
25 | line-height: 2.5rem;
26 | font-family: 'Exo 2', sans-serif;
27 | margin: 0;
28 | }
29 |
30 | a {
31 | display: inline-block;
32 | text-decoration: inherit;
33 | border: 1px solid white;
34 | color: white;
35 | border-radius: 0.5rem;
36 | font-family: 'Space Mono', monospace;
37 | text-transform: uppercase;
38 | padding: 0.5rem 1rem;
39 | margin-top: 2rem;
40 | font-size: 0.875rem;
41 | line-height: 1.25rem;
42 | }
43 |
44 | .content-wrapper a:hover {
45 | --tw-text-opacity: 1;
46 | color: rgba(31, 41, 55, var(--tw-text-opacity));
47 | background-color: white;
48 | }
49 |
50 | @media only screen and (max-width: 600px) {
51 | h1 {
52 | font-size: 0.8rem;
53 | line-height: 0.95rem;
54 | }
55 | p {
56 | width: 100%;
57 | font-size: 1.1rem;
58 | line-height: 1.1rem;
59 | }
60 | }
61 |
62 | #github {
63 | display: flex;
64 | justify-content: center;
65 | align-items: center;
66 | }
67 |
68 | #github a {
69 | display: flex;
70 | align-items: center;
71 | font-size: large;
72 | }
73 | #github svg {
74 | margin-right: 0.5rem;
75 | }
76 | #github a:hover svg path {
77 | fill: #161b22;
78 | }
79 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | module.exports = {
4 | resolve: {
5 | alias: {
6 | '@': path.resolve(__dirname, '.'),
7 | },
8 | },
9 | build: {
10 | assetsInlineLimit: '102400', // 2kb
11 | chunkSizeWarningLimit: '102400', // 2kb
12 | rollupOptions: {
13 | input: {
14 | main: path.resolve(__dirname, '/index.html'),
15 | customchair: path.resolve(__dirname, 'projects/custom-chairs/index.html'),
16 | plane: path.resolve(__dirname, 'projects/plane/index.html'),
17 | sphere: path.resolve(__dirname, 'projects/sphere/index.html'),
18 | shootout: path.resolve(__dirname, 'projects/shootout/index.html'),
19 | },
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------