1 import * as THREE from 'three'; 2 import * as dat from 'dat.gui'; 3 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'; 5 import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; 6 import Stats from 'three/examples/jsm/libs/stats.module.js'; 7 import { updateTrainCrankPosition } from '/src/train.js'; 8 import { generateTunnelGeometry } from '/src/tunnel.js'; 9 import { createInstancedTrees } from '/src/trees.js'; 10 import { elevationGeometry } from '/src/terrain.js'; 11 import { generateBridge } from '/src/bridge.js'; 12 import { buildTrain } from '/src/train.js'; 13 import { 14 getRailsPathPosAt, 15 buildRailsGeometry, 16 buildRailsFoundationGeometry 17 } from '/src/rails.js'; 18 19 let scene, camera, renderer, time, prevTime, gui, stats; 20 let terrainGeometry, terrain, treesForbiddenMapData, treesForbiddenMap, elevationMap, elevationMapData; 21 let firstPersonControls, orbitControls; 22 let train, trainLight, trainLight2, trainLight3; 23 24 let helpers = []; 25 let cameras = []; 26 let camerasName = []; 27 let objects = []; 28 let lights = { 29 ambient: { object: null }, 30 directional: { object: null }, 31 hemisphere: { object: null } 32 }; 33 34 let settings = { 35 animationEnable: false, 36 showTrain: true, 37 currCameraIndex: 0, 38 nightMode: true, 39 showHelpers: false, 40 showFps: true, 41 currCameraName: "", 42 shadows: false 43 }; 44 45 let raycaster; 46 47 let moveForward = false; 48 let moveForwardRunning = false; 49 let moveBackward = false; 50 let moveLeft = false; 51 let moveRight = false; 52 53 const velocity = new THREE.Vector3(); 54 const direction = new THREE.Vector3(); 55 56 // actualizar la variable global `amplitude` de '/src/track-map/' 57 const widthSegments = 150; 58 const heightSegments = 150; 59 const amplitude = 10; 60 const amplitudeBottom = -2.10; // terrain offset 61 62 import skyDayUrl from './assets/sky_day_void.jpg' 63 import skyNightUrl from './assets/sky_night.jpg' 64 import rocaUrl from './assets/roca.jpg' 65 import pastoUrl from './assets/pasto.jpg' 66 import tierraUrl from './assets/tierra.jpg' 67 import maderaUrl from './assets/madera.jpg' 68 import durmientesUrl from './assets/durmientes.jpg' 69 import elevationMapUrl from './assets/elevation_map_wider_river.png' 70 import treeForbiddenMapUrl from './assets/tree_forbidden_zone_map_wider_path.png' 71 72 const textures = { 73 skyDay: { url: skyDayUrl, object: null }, 74 skyNight: { url: skyNightUrl, object: null }, 75 roca: { url: rocaUrl, object: null }, 76 pasto: { url: pastoUrl, object: null }, 77 tierra: { url: tierraUrl, object: null }, 78 madera: { url: maderaUrl, object: null }, 79 durmientes: { url: durmientesUrl, object: null }, 80 elevationMap: { url: elevationMapUrl, object: null }, 81 treeForbiddenMap: { url: treeForbiddenMapUrl, object: null } 82 }; 83 84 function onResize() { 85 // const aspect = container.offsetWidth / container.offsetHeight; 86 const aspect = window.innerWidth / window.innerHeight; 87 88 for(let i = 0; i < cameras.length; ++i) { 89 if(cameras[i] != undefined) { 90 cameras[i].aspect = aspect; 91 cameras[i].updateProjectionMatrix(); 92 } 93 } 94 95 renderer.setSize( window.innerWidth, window.innerHeight ); 96 } 97 98 function updateCamera() { 99 orbitControls.enabled = false; 100 blocker.style.display = 'none'; 101 instructions.style.display = 'none'; 102 if(settings.nightMode == true) { 103 trainLight.intensity = 200; 104 trainLight.distance = 100; 105 } 106 107 firstPersonControls.unlock(); 108 109 let currCamera = cameras[settings.currCameraIndex]; 110 switch(currCamera.name) { 111 case "topView": 112 orbitControls.enabled = true; 113 break; 114 case "firstPersonCamera": 115 blocker.style.display = 'block'; 116 instructions.style.display = 'flex'; 117 break; 118 case "trainCamera": 119 case "trainConductorCamera": 120 // por alguna razon cuando la camara es `trainConductorCamera` 121 // o `trainCamera` la luz principal del tren se ve mas tenue 122 if(settings.nightMode == true) { 123 trainLight.intensity = 1000; 124 trainLight.distance = 1000; 125 } 126 break; 127 default: 128 break; 129 } 130 onResize(); 131 settings.currCameraName = camerasName[settings.currCameraIndex]; 132 } 133 134 function prevCamera() { 135 const camerasCount = cameras.length; 136 137 if(settings.currCameraIndex == 0) { 138 settings.currCameraIndex = (camerasCount - 1); 139 } else { 140 settings.currCameraIndex -= 1; 141 } 142 updateCamera(); 143 } 144 145 function nextCamera() { 146 const camerasCount = cameras.length; 147 148 if(settings.currCameraIndex == (camerasCount - 1)) { 149 settings.currCameraIndex = 0; 150 } else { 151 settings.currCameraIndex += 1; 152 } 153 154 updateCamera(); 155 } 156 157 const blocker = document.getElementById( 'blocker' ); 158 const instructions = document.getElementById( 'instructions' ); 159 160 function firstPersonCameraHandler(eventName) { 161 // if(cameras[settings.currCameraIndex].name != "firstPersonCamera") { 162 // console.log(cameras[settings.currCameraIndex].name); 163 // return; 164 // } 165 166 switch(eventName) { 167 case 'click': 168 console.log('click'); 169 firstPersonControls.lock(); 170 break; 171 case 'lock': 172 console.log('lock'); 173 instructions.style.display = 'none'; 174 blocker.style.display = 'none'; 175 break; 176 case 'unlock': 177 console.log('unlock'); 178 blocker.style.display = 'block'; 179 instructions.style.display = 'flex'; 180 break; 181 } 182 } 183 184 function keyHandler(event) { 185 if(event.type == 'keydown') { 186 switch (event.code) { 187 case 'ArrowUp': 188 case 'KeyW': 189 moveForward = true; 190 if(event.shiftKey) { 191 moveForwardRunning = true; 192 } 193 break; 194 case 'ArrowLeft': 195 case 'KeyA': 196 moveLeft = true; 197 break; 198 case 'ArrowDown': 199 case 'KeyS': 200 moveBackward = true; 201 break; 202 case 'ArrowRight': 203 case 'KeyD': 204 moveRight = true; 205 break; 206 case "KeyC": 207 if(event.shiftKey) { 208 prevCamera(); 209 } else { 210 nextCamera(); 211 } 212 if(gui != undefined) { 213 gui.__controllers[6].updateDisplay(); 214 } 215 break; 216 case 'Space': 217 // if (firstPersonControls.isLocked === true) { 218 // console.log(canJump); 219 // velocity.y += 350; 220 // break; 221 // } 222 settings.animationEnable = !settings.animationEnable; 223 if(gui != undefined) { 224 // update gui 'Animations' checkbox 225 gui.__controllers[0].updateDisplay(); 226 } 227 break; 228 } 229 switch(event.key) { 230 case "Shift": 231 if(!moveForwardRunning) { 232 moveForwardRunning = true; 233 } 234 break; 235 default: 236 break; 237 } 238 } else { 239 // key up 240 switch (event.code) { 241 case 'ArrowUp': 242 case 'KeyW': 243 moveForward = false; 244 moveForwardRunning = false; 245 break; 246 case 'ArrowLeft': 247 case 'KeyA': 248 moveLeft = false; 249 break; 250 case 'ArrowDown': 251 case 'KeyS': 252 moveBackward = false; 253 break; 254 case 'ArrowRight': 255 case 'KeyD': 256 moveRight = false; 257 break; 258 } 259 switch(event.key) { 260 case "Shift": 261 if(moveForwardRunning) { 262 moveForwardRunning = false; 263 } 264 break; 265 default: 266 break; 267 } 268 } 269 } 270 271 function setupFirstPersonControls() { 272 const firstPersonCamera = new THREE.PerspectiveCamera( 273 50, window.innerWidth / window.innerHeight, 0.1, 1000); 274 275 firstPersonCamera.position.set(0, 5, 20); 276 firstPersonCamera.lookAt(-10, 5, 0); 277 firstPersonCamera.name = "firstPersonCamera" 278 cameras.push(firstPersonCamera); 279 camerasName.push("Primera Persona"); 280 281 firstPersonControls = new PointerLockControls(firstPersonCamera, document.body); 282 283 284 instructions.addEventListener('click', function() { 285 // console.log(event); 286 firstPersonCameraHandler('click'); 287 }); 288 289 firstPersonControls.addEventListener('lock', function() { 290 // console.log(event); 291 firstPersonCameraHandler('lock'); 292 }); 293 294 firstPersonControls.addEventListener('unlock', function() { 295 // console.log(event); 296 firstPersonCameraHandler('unlock'); 297 }); 298 scene.add(firstPersonControls.getObject()); 299 300 window.addEventListener('keydown', (event) => { 301 keyHandler(event); 302 }); 303 304 window.addEventListener('keyup', (event) => { 305 keyHandler(event); 306 }); 307 } 308 309 function setupThreeJs() { 310 scene = new THREE.Scene(); 311 renderer = new THREE.WebGLRenderer(); 312 renderer.setPixelRatio( window.devicePixelRatio ); 313 renderer.setSize( window.innerWidth, window.innerHeight ); 314 renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap 315 renderer.shadowMap.enabled = settings.shadows; 316 317 document.body.appendChild(renderer.domElement); 318 319 stats = new Stats(); 320 if(settings.showFps == true) { 321 document.body.appendChild(stats.dom); 322 } 323 324 const topView = new THREE.PerspectiveCamera( 325 35, window.innerWidth / window.innerHeight, 0.1, 1000); 326 327 topView.position.set(-32, 38, 70); 328 topView.lookAt(0, 0, 0); 329 topView.name = "topView" 330 cameras.push(topView); 331 camerasName.push("Vista Global"); 332 333 orbitControls = new OrbitControls(topView, renderer.domElement); 334 335 lights.ambient.object = new THREE.AmbientLight(0xffffff); 336 337 lights.hemisphere.object = new THREE.HemisphereLight(0xFFFFFF, 0x000000, 0.25); 338 339 lights.directional.object = new THREE.DirectionalLight(0xffffff, 1); 340 lights.directional.object.position.set(-35, 35, 35); 341 342 // Set up shadow properties for the light 343 lights.directional.object.castShadow = true; 344 lights.directional.object.shadow.mapSize.width = 512; 345 lights.directional.object.shadow.mapSize.height = 512; 346 347 lights.directional.object.shadow.camera = new THREE.OrthographicCamera( 348 -65, 65, 45, -35, 1.0, 112); 349 350 const directionalLightShadowsHelper = new THREE.CameraHelper(lights.directional.object.shadow.camera); 351 directionalLightShadowsHelper.visible = settings.showHelpers; 352 scene.add(directionalLightShadowsHelper); 353 helpers.push(directionalLightShadowsHelper); 354 355 scene.add(lights.ambient.object); 356 scene.add(lights.hemisphere.object); 357 scene.add(lights.directional.object); 358 359 if(settings.nightMode == true) { 360 lights.ambient.object.visible = false; 361 lights.hemisphere.object.intensity = 0; 362 lights.directional.object.color.setHex(0xcdddfe); // 0x090254; 0xa8a1fd 363 scene.background = textures.skyNight.object; 364 lights.directional.object.position.set(35, 35, 35); // match the skybox texture moon light position 365 } else { 366 lights.ambient.object.visible = true; 367 lights.hemisphere.object.intensity = 1; 368 lights.directional.object.intensity = 1; 369 lights.directional.object.color.setHex(0xFFFFFF); 370 scene.background = textures.skyDay.object; 371 lights.directional.object.position.set(-35, 35, 35); 372 } 373 374 const hemisphereLightHelper = new THREE.HemisphereLightHelper(lights.hemisphere.object, 5); 375 helpers.push(hemisphereLightHelper); 376 377 const directionalLightHelper = new THREE.DirectionalLightHelper(lights.directional.object, 5); 378 helpers.push(directionalLightHelper); 379 380 const gridHelper = new THREE.GridHelper(100, 100); 381 helpers.push(gridHelper); 382 383 const axesHelper = new THREE.AxesHelper(5); 384 helpers.push(axesHelper); 385 386 for(let i = 0; i < helpers.length; ++i) { 387 helpers[i].visible = settings.showHelpers; 388 scene.add(helpers[i]); 389 } 390 391 window.addEventListener('resize', onResize); 392 onResize(); 393 394 textures.skyDay.object.mapping = THREE.EquirectangularRefractionMapping; 395 textures.skyNight.object.mapping = THREE.EquirectangularRefractionMapping; 396 397 if(settings.nightMode == true) { 398 scene.background = textures.skyNight.object; 399 } else { 400 scene.background = textures.skyDay.object; 401 } 402 } 403 404 function onTextureLoaded(key, texture) { 405 texture.wrapS = texture.wrapT = THREE.RepeatWrapping; 406 textures[key].object = texture; 407 console.log('Texture `' + key + '` loaded'); 408 } 409 410 function loadTextures(callback) { 411 const loadingManager = new THREE.LoadingManager(); 412 413 loadingManager.onLoad = () => { 414 console.log('All textures loaded'); 415 callback(); 416 }; 417 418 for (const key in textures) { 419 console.log("Loading textures"); 420 const loader = new THREE.TextureLoader(loadingManager); 421 const texture = textures[key]; 422 texture.object = loader.load( 423 texture.url, 424 onTextureLoaded.bind(this, key), 425 null, 426 (error) => { 427 console.error(error); 428 } 429 ); 430 } 431 } 432 433 function buildBridge() { 434 // const bridge1 = generateBridge(); 435 // const bridge2 = generateBridge(); 436 437 // (arcCount, arcRadius, columnWidth, columnHeight, padding, squaresCount, squareLen) 438 const bridge1 = generateBridge(1, 3, 0, 0, 10, 2, 2); 439 const bridge2 = generateBridge(2, 2, 1, 0, 15, 3, 2); 440 441 bridge1.scale.set(0.5, 0.5, 0.5); 442 bridge1.position.set(16, -0.75, 36); 443 // bridge1.rotateY(-Math.PI*0.118); 444 445 bridge2.scale.set(0.5, 0.5, 0.5); 446 bridge2.position.set(-14, -0.25, -41); 447 // bridge2.rotateY(-Math.PI*0.118); 448 449 const bridgeCamera = new THREE.PerspectiveCamera( 450 55, window.innerWidth / window.innerHeight, 0.1, 10000); 451 452 bridgeCamera.position.set(-18, 11, -2.75); 453 bridgeCamera.lookAt(50, 0, 42); 454 bridge2.add(bridgeCamera); 455 bridgeCamera.name = "bridgeCamera"; 456 cameras.push(bridgeCamera); 457 camerasName.push("Vista del Puente"); 458 459 bridge1.castShadow = true; 460 bridge1.receiveShadow = true; 461 bridge2.castShadow = true; 462 bridge2.receiveShadow = true; 463 464 scene.add(bridge1); 465 scene.add(bridge2); 466 467 objects.push(bridge1); 468 objects.push(bridge2); 469 } 470 471 // loco -> locomotora/locomotive 472 function buildLoco() { 473 train = buildTrain(); 474 475 const trainConductorCamera = new THREE.PerspectiveCamera( 476 55, window.innerWidth / window.innerHeight, 0.1, 10000); 477 478 trainConductorCamera.position.set(0, 7, -11); 479 trainConductorCamera.lookAt(0, 20, 100); 480 train.add(trainConductorCamera); 481 trainConductorCamera.name = "trainConductorCamera"; 482 cameras.push(trainConductorCamera); 483 camerasName.push("Cabina del Tren"); 484 485 const trainCamera = new THREE.PerspectiveCamera( 486 55, window.innerWidth / window.innerHeight, 0.1, 10000); 487 488 trainCamera.position.set(-12, 6, -20); 489 trainCamera.lookAt(0, 10, 15); 490 train.add(trainCamera); 491 trainCamera.name = `trainCamera`; 492 cameras.push(trainCamera); 493 camerasName.push("Costado del Tren"); 494 495 const trainBackCamera = new THREE.PerspectiveCamera( 496 55, window.innerWidth / window.innerHeight, 0.1, 10000); 497 498 trainBackCamera.position.set(0, 16, -10); 499 trainBackCamera.lookAt(0, 18, -100); 500 train.add(trainBackCamera); 501 trainBackCamera.name = "trainBackCamera"; 502 cameras.push(trainBackCamera); 503 camerasName.push("Vista hacia atras desde la Cabina del Tren"); 504 505 // SpotLight(color: Int, intensity: Float, distance: Float, angle: Radians, penumbra: Float, decay: Float) 506 trainLight = new THREE.SpotLight(0xffffff, 200.0, 100.0, Math.PI/6, 0.5, 1.0); 507 train.add(trainLight.target); 508 train.add(trainLight); 509 trainLight.position.set(0, 4, 5); 510 trainLight.target.position.set(0, -100, 1000); 511 trainLight.target.updateMatrixWorld(); 512 513 trainLight2 = new THREE.SpotLight(0xffffff, 10.0, 3.0, Math.PI/6, 0.5, 0.5); 514 train.add(trainLight2.target); 515 train.add(trainLight2); 516 trainLight2.position.set(0, 3.25, 15); 517 trainLight2.target.position.set(0, 0, -100); 518 trainLight2.target.updateMatrixWorld(); 519 520 trainLight3 = new THREE.SpotLight(0xffffff, 10.0, 16.0, Math.PI/3, 0.5, 0.5); 521 train.add(trainLight3.target); 522 train.add(trainLight3); 523 trainLight3.position.set(0, 5, 5); 524 trainLight3.target.position.set(0, -25, 100); 525 trainLight3.target.updateMatrixWorld(); 526 527 //Set up shadow properties for the light 528 trainLight.castShadow = true; 529 trainLight.shadow.mapSize.width = 256; 530 trainLight.shadow.mapSize.height = 256; 531 trainLight.shadow.camera.near = 0.5; 532 trainLight.shadow.camera.far = 40; 533 trainLight.shadow.focus = 1; 534 535 trainLight3.castShadow = true; 536 trainLight3.shadow.mapSize.width = 128; 537 trainLight3.shadow.mapSize.height = 128; 538 trainLight3.shadow.camera.near = 0.5; 539 trainLight3.shadow.camera.far = 25; 540 trainLight3.shadow.focus = 1; 541 542 const trainLightHelper = new THREE.CameraHelper(trainLight.shadow.camera); 543 const trainLight3Helper = new THREE.CameraHelper(trainLight3.shadow.camera); 544 545 trainLight.visible = settings.nightMode; 546 trainLight2.visible = settings.nightMode; 547 trainLight3.visible = settings.nightMode; 548 549 trainLightHelper.visible = settings.showHelpers; 550 trainLight3Helper.visible = settings.showHelpers; 551 552 helpers.push(trainLightHelper); 553 helpers.push(trainLight3Helper); 554 555 train.scale.set(0.145, 0.145, 0.145); 556 train.visible = settings.showTrain; 557 scene.add(train); 558 } 559 560 function buildRailsFoundation() { 561 const railsFoundationGeometry = buildRailsFoundationGeometry(); 562 563 textures.durmientes.object.wrapS = THREE.RepeatWrapping; 564 textures.durmientes.object.wrapT = THREE.RepeatWrapping; 565 textures.durmientes.object.repeat.set(1, 150); 566 textures.durmientes.object.anisotropy = 16; 567 568 // load into `map` the example texture 569 // const map = new THREE.TextureLoader().load( 570 // 'https://threejs.org/examples/textures/uv_grid_opengl.jpg'); 571 // map.wrapS = map.wrapT = THREE.RepeatWrapping; 572 // map.repeat.set(1, 80); 573 // map.anisotropy = 16; 574 // map.rotation = Math.PI/2; 575 576 const railsFoundationMaterial = new THREE.MeshPhongMaterial({ 577 side: THREE.FrontSide, 578 map: textures.durmientes.object 579 // map: map 580 }); 581 582 const railsFoundation = new THREE.Mesh(railsFoundationGeometry, railsFoundationMaterial); 583 railsFoundation.receiveShadow = true; 584 railsFoundation.castShadow = true; 585 railsFoundation.position.set(-1, 1.25, -1); 586 railsFoundation.scale.set(1.00, 1.50, 1.00); 587 scene.add(railsFoundation); 588 // descomentando esto se tiene en cuenta la altura del terraplen de las vias 589 // para la camara en primera person pero resulta en muy baja performance 590 // objects.push(railsFoundation); 591 } 592 593 function buildRails() { 594 const railsGeometry = buildRailsGeometry(); 595 const railsMaterial = new THREE.MeshPhongMaterial({ 596 side: THREE.BackSide, 597 color: 0xFFFFFF 598 }); 599 600 const rails = new THREE.Mesh(railsGeometry, railsMaterial); 601 rails.castShadow = true; 602 rails.receiveShadow = true; 603 rails.position.set(-1, 1.25, -1); 604 rails.scale.set(1.00, 1.50, 1.00); 605 scene.add(rails); 606 } 607 608 function buildTerrainCustomMaterial() { 609 const customMaterial = new THREE.MeshPhongMaterial({ 610 color: 0xffffff, 611 specular: 0x333333, 612 side: THREE.FrontSide, 613 }); 614 615 // definos las variables uniformes adicionales que necesitamos 616 let additionalUniforms = { 617 // grassTexture: { value: grassTexture, type: 't' }, 618 // rockTexture: { value: rockTexture, type: 't' }, 619 620 dirtSampler: { type: 't', value: textures.tierra.object }, 621 rockSampler: { type: 't', value: textures.roca.object }, 622 grassSampler: { type: 't', value: textures.pasto.object }, 623 scale: { type: 'f', value: 3.0 }, 624 terrainAmplitude: { type: 'f', value: amplitude }, 625 terrainAmplitudeBottom: { type: 'f', value: amplitudeBottom }, 626 worldNormalMatrix: { type: 'm4', value: null }, 627 dirtStepWidth: { type: 'f', value: 0.20 }, 628 rockStepWidth: { type: 'f', value: 0.15 }, 629 }; 630 // le decimos al material que vamos a usar UVs para que incluya las coordenadas UV en el shader 631 customMaterial.defines = { USE_UV: true }; 632 633 // Este callback se ejecuta antes de compilar el shader 634 // Hay que ver como referencia el archivo 635 // node_modules/three/src/renderers/shaders/ShaderLib/meshphong.glsl.js 636 // para saber que chunks podemos reemplazar 637 638 customMaterial.onBeforeCompile = function (shader) { 639 // le agregamos las variables uniformes adicionales al shader 640 shader.uniforms.dirtSampler = additionalUniforms.dirtSampler; 641 shader.uniforms.rockSampler = additionalUniforms.rockSampler; 642 shader.uniforms.grassSampler = additionalUniforms.grassSampler; 643 shader.uniforms.scale = additionalUniforms.scale; 644 shader.uniforms.terrainAmplitude = additionalUniforms.terrainAmplitude; 645 shader.uniforms.terrainAmplitudeBottom = additionalUniforms.terrainAmplitudeBottom; 646 shader.uniforms.worldNormalMatrix = additionalUniforms.worldNormalMatrix; 647 shader.uniforms.dirtStepWidth = additionalUniforms.dirtStepWidth; 648 shader.uniforms.rockStepWidth = additionalUniforms.rockStepWidth; 649 650 // hacemos un search and replace en el vertex shader 651 // buscamos la linea que dice 652 // vViewPosition = - mvPosition.xyz; 653 // y le agregamos una linea mas que guarde la posicion del vertice en el espacio del mundo 654 shader.vertexShader = shader.vertexShader.replace( 655 'vViewPosition = - mvPosition.xyz;', 656 `vViewPosition = - mvPosition.xyz; 657 vWorldPosition = (modelMatrix*vec4(transformed,1.0)).xyz;` 658 ); 659 660 // agregamos una variable varying al comienzo del vertex shader 661 // para pasar la posicion del vertice en coordenadas del mundo al fragment shader 662 shader.vertexShader = 663 `varying vec3 vWorldPosition; 664 ` + shader.vertexShader; 665 666 // agregamos las variables uniformes y varying al fragment shader 667 // Siempre hay que tener cuidado con los nombres de las variables que definimos 668 // no deben coincidir con las variables que usa Three.js 669 670 shader.fragmentShader = ` 671 uniform float scale; 672 uniform float terrainAmplitude; 673 uniform float terrainAmplitudeBottom; 674 uniform float dirtStepWidth; 675 uniform float rockStepWidth; 676 677 uniform sampler2D dirtSampler; 678 uniform sampler2D rockSampler; 679 uniform sampler2D grassSampler; 680 varying vec3 vWorldPosition; 681 682 ` + shader.fragmentShader; 683 684 shader.fragmentShader = shader.fragmentShader.replace( 685 'void main() {', 686 ` 687 float myNormalizeFunc(float inputValue, float minValue, float maxValue) { 688 return (inputValue - minValue) / (maxValue - minValue); 689 } 690 691 void main () { 692 float heightFactor = vWorldPosition.y - terrainAmplitudeBottom; 693 float heightFactorNormalized = myNormalizeFunc(heightFactor, 0.0, terrainAmplitude); 694 `); 695 696 // reemplazamos el include del chunk map_fragment por nuestro propio codigo 697 shader.fragmentShader = shader.fragmentShader.replace( 698 '#include <map_fragment>', 699 `// calculamos las coordenadas UV en base a las coordenadas de mundo 700 vec2 uvCoords=vWorldPosition.xz/100.0; 701 vec2 myUV = uvCoords*8.0; 702 vec2 myUV2 = uvCoords*scale; 703 704 vec3 grass = texture2D(grassSampler, uvCoords).xyz; 705 vec3 dirt = texture2D(dirtSampler, uvCoords*4.0).xyz; 706 vec3 rock = texture2D(rockSampler, uvCoords).xyz; 707 708 // si quisieramos podriamos usar la variabl vUv tambien que son las coordenadas UV del vertice 709 710 // muestreo de pasto a diferentes escalas, luego se combina con \`mix()\` 711 vec3 grass1 = texture2D(grassSampler, myUV2*1.00).xyz; 712 vec3 grass2 = texture2D(grassSampler, myUV2*3.13).xyz; 713 vec3 grass3 = texture2D(grassSampler, myUV2*2.37).xyz; 714 vec3 colorGrass = mix(mix(grass1,grass2,0.5),grass3,0.3); 715 716 // lo mismo para la textura de tierra 717 vec3 dirt1 = texture2D(dirtSampler, myUV2*3.77).xyz; 718 vec3 dirt2 = texture2D(dirtSampler, myUV2*1.58).xyz; 719 vec3 dirt3 = texture2D(dirtSampler, myUV2*1.00).xyz; 720 vec3 colorDirt = mix(mix(dirt1, dirt2, 0.5), dirt3, 0.3); 721 722 // lo mismo para la textura de roca 723 vec3 rock1 = texture2D(rockSampler,myUV2*0.40).xyz; 724 vec3 rock2 = texture2D(rockSampler,myUV2*2.38).xyz; 725 vec3 rock3 = texture2D(rockSampler,myUV2*3.08).xyz; 726 vec3 colorRock = mix(mix(rock1, rock2, 0.5), rock3,0.5); 727 728 float u = heightFactorNormalized; 729 730 float width2 = rockStepWidth; 731 float rockFactor = 2.00 - smoothstep(0.0, width2, u) - smoothstep(1.0, 1.00 - width2, u); 732 733 float width = dirtStepWidth; 734 float s1 = smoothstep(0.00, width, u); 735 float s2 = smoothstep(width, width*2.0, u); 736 float s3 = smoothstep(0.50, 0.50 + width, u); 737 float s4 = smoothstep(0.50 + width, 0.50 + width*2.0, u); 738 float dirtFactor = (s1 - s2) + (s3 - s4); 739 740 float grassFactor = smoothstep(0.0, 0.35, u) - smoothstep(0.35, 1.00, u); 741 742 vec3 colorDirtGrass = mix(colorDirt, colorGrass, grassFactor); 743 vec3 colorDirtGrassDirt = mix(colorDirtGrass, colorDirt, dirtFactor); 744 vec3 color = mix(colorDirtGrassDirt, colorRock, rockFactor); 745 746 diffuseColor = vec4(color, 1.0); 747 748 // leemos los colores de las texturas 749 // vec4 grassColor = texture2D(grassSampler,uvCoords); 750 // vec4 rockColor = texture2D(rockSampler,uvCoords); 751 752 // mezclamos los colores en base a la altura del vertice 753 // diffuseColor = mix(grassColor, rockColor, smoothstep(0.0,5.0,vWorldPosition.y));`); 754 755 // imprimimos el shader para debuggear 756 // console.log(shader.vertexShader); 757 // console.log(shader.fragmentShader); 758 }; 759 return customMaterial; 760 } 761 762 function buildTerrain() { 763 const width = 100; 764 const height = 100; 765 766 terrainGeometry = elevationGeometry( 767 width, height, 768 amplitude, 769 widthSegments, heightSegments, 770 textures.elevationMap.object); 771 772 console.log('Applying textures'); 773 774 const customMaterial = buildTerrainCustomMaterial(); 775 terrain = new THREE.Mesh(terrainGeometry, customMaterial); 776 777 terrain.castShadow = true; 778 terrain.receiveShadow = true; 779 780 scene.add(terrain); 781 782 terrain.position.set(0, amplitudeBottom, 0); 783 objects.push(terrain); 784 785 console.log('Generating water'); 786 const waterGeometry = new THREE.PlaneGeometry(width/2, height-1.25); 787 const waterMaterial = new THREE.MeshPhongMaterial( {color: 0x12ABFF, side: THREE.BackSide} ); 788 const water = new THREE.Mesh( waterGeometry, waterMaterial ); 789 water.rotateX(Math.PI/2); 790 water.position.set(0, 0, -0.65); 791 792 water.castShadow = false; 793 water.receiveShadow = true; 794 scene.add(water); 795 } 796 797 function buildTunnel() { 798 // tunnelHeight = 20, tunnelWidth = 14, tunnelWallThickness = 0.5, tunnelLen = 26 799 const tunnelGeometry = generateTunnelGeometry(24, 12, 0.5, 46); 800 801 textures.madera.object.wrapS = THREE.RepeatWrapping; 802 textures.madera.object.wrapT = THREE.RepeatWrapping; 803 textures.madera.object.repeat.set(0.10, 0.10); 804 textures.madera.object.anisotropy = 16; 805 806 const tunnelMaterial = new THREE.MeshPhongMaterial({ 807 side: THREE.FrontSide, 808 map: textures.madera.object 809 }); 810 811 const tunnel = new THREE.Mesh(tunnelGeometry, tunnelMaterial) ; 812 tunnel.castShadow = true; 813 tunnel.receiveShadow = true; 814 tunnel.scale.set(0.5, 0.5, 0.5); 815 816 const trainPathPos = getRailsPathPosAt(0.32); 817 tunnel.position.set(trainPathPos[0].x, 0, trainPathPos[0].z); 818 tunnel.lookAt(trainPathPos[1].x*1000, 0, trainPathPos[1].z*1000); 819 820 scene.add(tunnel); 821 822 const tunnelCamera = new THREE.PerspectiveCamera( 823 65, window.innerWidth / window.innerHeight, 0.1, 10000); 824 825 tunnelCamera.position.set(-1, 12, 18); 826 tunnelCamera.lookAt(0, 10, -10); 827 tunnelCamera.name = "tunnelCamera"; 828 tunnel.add(tunnelCamera); 829 cameras.push(tunnelCamera); 830 camerasName.push("Camara del Tunel"); 831 } 832 833 function buildTrees(count = 50) { 834 const [treeLogs, treeLeaves] = createInstancedTrees(count); 835 scene.add(treeLogs); 836 scene.add(treeLeaves); 837 838 treeLogs.castShadow = true; 839 treeLogs.receiveShadow = true; 840 treeLeaves.castShadow = true; 841 treeLeaves.receiveShadow = true; 842 } 843 844 function toggleNightMode() { 845 console.log("Toggling night mode"); 846 console.log(settings.nightMode); 847 if(settings.nightMode == true) { 848 lights.ambient.object.visible = false; 849 lights.hemisphere.object.intensity = 0; 850 lights.directional.object.color.setHex(0xcdddfe); // 0x090254; 0xa8a1fd 851 scene.background = textures.skyNight.object; 852 lights.directional.object.position.set(35, 35, 35); // match the skybox texture moon light 853 trainLight.visible = true; 854 trainLight2.visible = true; 855 trainLight3.visible = true; 856 } else { 857 lights.ambient.object.visible = true; 858 lights.hemisphere.object.intensity = 1; 859 lights.directional.object.intensity = 1; 860 lights.directional.object.color.setHex(0xFFFFFF); 861 scene.background = textures.skyDay.object; 862 lights.directional.object.position.set(-35, 35, 35); 863 trainLight.visible = false; 864 trainLight2.visible = false; 865 trainLight3.visible = false; 866 } 867 } 868 869 function createMenu() { 870 gui = new dat.GUI({ width: 250 }); 871 gui.add(settings, 'animationEnable', true).name('Animaciones'); 872 gui.add(settings, 'showTrain').name('Mostrar tren').onChange( 873 function () { 874 train.visible = !train.visible; 875 } 876 ); 877 gui.add(settings, 'nightMode', false).name('Modo noche').onChange(toggleNightMode); 878 gui.add(settings, 'showHelpers', true).name('Mostrar Guias').onChange( 879 function() { 880 for(let i = 0; i < helpers.length; ++i) { 881 helpers[i].visible = settings.showHelpers; 882 scene.add(helpers[i]); 883 } 884 } 885 ); 886 gui.add(settings, 'showFps', true).name('Mostrar FPS').onChange( 887 function() { 888 if(settings.showFps == true) { 889 document.body.appendChild(stats.dom); 890 } else { 891 document.body.removeChild(stats.dom); 892 } 893 } 894 ); 895 gui.add(settings, 'shadows', true).name('Sombras').onChange( 896 function() { 897 renderer.shadowMap.enabled = settings.shadows; 898 scene.traverse(function (child) { 899 if (child.material) { 900 child.material.needsUpdate = true 901 } 902 }); 903 } 904 ); 905 gui.add(settings, "currCameraName", camerasName).name('Camara').setValue(camerasName[settings.currCameraIndex]).onChange( 906 function() { 907 settings.currCameraIndex = camerasName.indexOf(settings.currCameraName); 908 updateCamera(); 909 } 910 ); 911 } 912 913 function buildScene() { 914 console.log('Building scene'); 915 buildTunnel(); 916 buildTrees(200); 917 buildTerrain(); 918 buildRailsFoundation(); 919 buildRails(); 920 buildLoco(); 921 buildBridge(); 922 } 923 924 function mainLoop() { 925 requestAnimationFrame(mainLoop); 926 stats.begin(); 927 928 const dt = 0.001; 929 if(settings.animationEnable) { 930 time = (time < 1.0-dt) ? (time + dt) : 0.00; 931 } 932 933 if(train.visible) { 934 updateTrainCrankPosition(time*200); 935 const trainPos = getRailsPathPosAt(time); 936 const railsData = getRailsPathPosAt(time); 937 938 let x = railsData[0].x; 939 let z = railsData[0].z; 940 941 train.position.set(-1+x, 2.30, -1+z); 942 train.lookAt(railsData[1].x*1000, 1.9, railsData[1].z*1000); 943 } 944 945 let time2 = performance.now(); 946 const firstPersonCameraHeight = 1.60; 947 if (firstPersonControls.isLocked === true) { 948 raycaster = new THREE.Raycaster(); 949 var raycasterPos = new THREE.Vector3(); 950 raycasterPos.copy(firstPersonControls.getObject().position) 951 raycasterPos.y += 2; 952 var raycasterDir = new THREE.Vector3(0, -1, 0); 953 954 raycaster.set(raycasterPos, raycasterDir); 955 const intersections = raycaster.intersectObjects(objects); 956 let positionY; 957 if((intersections == undefined) || (intersections[0] == undefined)) { 958 positionY = 0.0; 959 } else { 960 positionY = intersections[0].point.y; 961 } 962 963 const delta = (time2 - prevTime) / 1000; 964 965 velocity.x -= velocity.x * 11.0 * delta; 966 velocity.z -= velocity.z * 11.0 * delta; 967 velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass 968 969 direction.z = Number( moveForward ) - Number( moveBackward ); 970 direction.x = Number( moveRight ) - Number( moveLeft ); 971 direction.normalize(); // this ensures consistent movements in all directions 972 973 if (moveForward || moveBackward) { 974 if(moveForwardRunning) { 975 velocity.z -= direction.z * 200.0 * delta; 976 } else { 977 velocity.z -= direction.z * 100.0 * delta; 978 } 979 } 980 981 if (moveLeft || moveRight) { 982 velocity.x -= direction.x * 100.0 * delta; 983 } 984 985 // TODO: terrain limits 986 firstPersonControls.moveRight(-velocity.x * delta); 987 firstPersonControls.moveForward(-velocity.z * delta); 988 989 firstPersonControls.getObject().position.y = 990 positionY < 0.0 ? firstPersonCameraHeight : positionY + firstPersonCameraHeight; 991 992 993 if (firstPersonControls.getObject().position.y < (positionY + firstPersonCameraHeight)) { 994 velocity.y = 0; 995 firstPersonControls.getObject().position.y = firstPersonCameraHeight; 996 // canJump = true; 997 } 998 } 999 1000 prevTime = time2; 1001 renderer.render(scene, cameras[settings.currCameraIndex]); 1002 stats.end(); 1003 } 1004 1005 function main() { 1006 setupThreeJs(); 1007 setupFirstPersonControls(); 1008 time = 0.90; 1009 prevTime = performance.now(); 1010 buildScene(); 1011 createMenu(); 1012 updateCamera(); 1013 mainLoop(); 1014 } 1015 1016 loadTextures(main);