├── .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 App

Click here
to design your own chair 3 | 4 |

Fully customizable with complex textures like wood, metal, silk etc

5 | GIF 6 |
7 |
8 | 9 | 10 | # 11 |

Shootout (Not ready for production)

Play an in-browser shooting game

12 | GIF 13 | 14 |
15 | 16 | # 17 | 18 |

Flappy Plane

Click here to play an in-browser game inspired by flappy birds

19 | GIF 20 | 21 |
22 | 23 | # 24 | 25 |

Sparkling Waves

Click here to check out basically the landing page of my website

26 | GIF 27 |
28 |
29 | 30 | # 31 | 32 |

Feel The Sphere

Click here to play with an awesome looking metalic ball :P

33 | GIF 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 | GIF 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 | Rikki407's Github Stats 69 | 70 |
71 | 72 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=Rikki407&layout=compact&text_color=daf7dc&bg_color=151515)](https://github.com/Rikki407/github-readme-stats) 73 | 74 |

🤝🏻 Connect with Me

75 | 76 |

77 |   78 |   79 |   80 |

81 | GIF 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 |
25 | 26 | 32 | Github 37 | 38 |
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 |
76 |
77 |
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 |
129 |
130 |
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 | 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 | ammo-icon 101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | health-icon 115 |
116 |
117 |
118 | play game 119 |
120 |
121 | gameover 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 | --------------------------------------------------------------------------------