tp/src/standalone/track-map.js (13618B)
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 { vertexShader, fragmentShader } from '/src/shaders.js'; 5 6 let scene, camera, renderer, container, terrainMaterial, terrainGeometry, terrain; 7 let treesForbiddenMapData, treesForbiddenMap, elevationMap, elevationMapData; 8 9 const widthSegments = 100; 10 const heightSegments = 100; 11 const amplitude = 8; 12 const amplitudeBottom = -1.00; 13 14 import tierraUrl from '../assets/tierra.jpg' 15 import rocaUrl from '../assets/roca.jpg' 16 import pastoUrl from '../assets/pasto.jpg' 17 import elevationMapUrl from '../assets/elevation_map_wider_river.png' 18 import treeForbiddenMapUrl from '../assets/tree_forbidden_zone_map_wider_path.png' 19 20 const textures = { 21 tierra: { url: tierraUrl, object: null }, 22 roca: { url: rocaUrl, object: null }, 23 pasto: { url: pastoUrl, object: null }, 24 elevationMap: { url: elevationMapUrl, object: null }, 25 treeForbiddenMap: { url: treeForbiddenMapUrl, object: null } 26 }; 27 28 function onResize() { 29 camera.aspect = container.offsetWidth / container.offsetHeight; 30 camera.updateProjectionMatrix(); 31 renderer.setSize(container.offsetWidth, container.offsetHeight); 32 } 33 34 function setupThreeJs() { 35 scene = new THREE.Scene(); 36 container = document.getElementById('mainContainer'); 37 38 renderer = new THREE.WebGLRenderer(); 39 renderer.setClearColor(0x606060); 40 container.appendChild(renderer.domElement); 41 42 camera = new THREE.PerspectiveCamera( 43 35, window.innerWidth/window.innerHeight, 0.1, 1000); 44 camera.position.set(100, 120, -100); 45 camera.lookAt(0, 0, 0); 46 47 const controls = new OrbitControls(camera, renderer.domElement); 48 49 const ambientLight = new THREE.AmbientLight(0xffffff); 50 scene.add(ambientLight); 51 52 const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25); 53 //scene.add(hemisphereLight); 54 55 const directionalLight = new THREE.DirectionalLight(0xffffff, 1); 56 directionalLight.position.set(100, 100, 100); 57 scene.add(directionalLight); 58 59 const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5); 60 // scene.add(directionalLightHelper); 61 62 const gridHelper = new THREE.GridHelper(150, 150); 63 scene.add(gridHelper); 64 65 const axesHelper = new THREE.AxesHelper( 5 ); 66 scene.add( axesHelper ); 67 68 window.addEventListener('resize', onResize); 69 onResize(); 70 } 71 72 const imgWidth = 512; 73 const imgHeight = 512; 74 75 // (x, y) ∈ [imgHeight, imgWidth] -> son un punto de la imagen 76 function getPixelIndex(x, y) { 77 return Math.floor(x + y*imgWidth*4); 78 } 79 80 function getPixel(imgData, index) { 81 let i = index*4, d = imgData.data 82 return [d[i],d[i+1],d[i+2],d[i+3]] // Returns array [R,G,B,A] 83 } 84 85 function getPixelXY(imgData, x, y) { 86 return getPixel(imgData, y*imgData.width+x) 87 } 88 89 // position: Vector3 90 function isForbbidenPosition(position) { 91 const x = Math.floor(position.x); 92 const y = position.y; 93 const z = Math.floor(position.z); 94 95 if((y > 5.0) || (y < 2.65)){ 96 console.log("(" + position.x + ", " + position.y + ", " + position.z + ") is not valid "); 97 return true; 98 } 99 100 let pixelArray = getPixelXY(treesForbiddenMap, x, z); 101 const R = pixelArray[0]; // Red 102 const G = pixelArray[1]; // Green 103 const B = pixelArray[2]; // Blue 104 const A = pixelArray[3]; // Alpha 105 // const pixel = new THREE.Vector4(R, G, B, A); 106 107 if(((R <= 10) && (G >= 250) && (B <= 10)) 108 || (R <= 80) && (G <= 80) && (B <= 80) 109 || (R >= 200) && (G >= 200) && (B >= 200)) { 110 // console.log("(" + position.x + ", " + position.y + ", " + position.z + ") is not valid "); 111 return true; 112 } 113 114 console.log("(" + position.x + ", " + position.y + ") is valid "); 115 return false; 116 } 117 118 // obtiene una posicion aleatoria en el terreno, para obtener la altura del 119 // terreno utiliza el mapa de elevacion. 120 // `padding` permite definir un borde del cual no se toman puntos 121 function getRandomPositionInTerrain(padding = 0) { 122 const x = Math.floor(Math.random() * (widthSegments-(padding*2))); 123 const z = Math.floor(Math.random() * (heightSegments-(padding*2))); 124 125 const pixelArray = getPixelXY(elevationMap, x, z); // array [R,G,B,A] 126 const y = (pixelArray[0]/255)*amplitude; 127 128 const position = new THREE.Vector3(x+padding, y, z+padding); 129 return position; 130 } 131 132 function createInstancedTrees(count) { 133 console.log('Generating `' + count + '` instances of tree'); 134 135 let logHeight = 4.0; 136 const treeLogGeometry = new THREE.CylinderGeometry( 137 0.30, 0.30, logHeight, 40, 40); 138 treeLogGeometry.translate(0, logHeight/2.0, 0); 139 const instancedTreeLogGeometry = new THREE.InstancedBufferGeometry(); 140 instancedTreeLogGeometry.copy(treeLogGeometry); 141 const treeLogMaterial = new THREE.MeshPhongMaterial({color: 0x7c3f00}); 142 const instancedTreeLogs = new THREE.InstancedMesh( 143 instancedTreeLogGeometry, 144 treeLogMaterial, 145 count); 146 147 const treeLeavesGeometry = new THREE.SphereGeometry(1.75,40,40); 148 const instancedTreeLeavesGeometry = new THREE.InstancedBufferGeometry(); 149 instancedTreeLeavesGeometry.copy(treeLeavesGeometry); 150 const treeLeavesMaterial = new THREE.MeshPhongMaterial({color: 0x365829}); 151 const instancedTreeLeaves = new THREE.InstancedMesh( 152 instancedTreeLeavesGeometry, 153 treeLeavesMaterial, 154 count); 155 156 const rotMatrix = new THREE.Matrix4(); 157 const translationMatrix = new THREE.Matrix4(); 158 const treeLogMatrix = new THREE.Matrix4(); 159 const treeLeavesMatrix = new THREE.Matrix4(); 160 161 const treesBorderPadding = 3.0; 162 for (let i = 0; i < count; i++) { 163 let position = getRandomPositionInTerrain(treesBorderPadding); 164 for(let j = 0; isForbbidenPosition(position); ++j) { 165 position = getRandomPositionInTerrain(treesBorderPadding); 166 if(j++ == 1000) { // maximo de iteraciones 167 break; 168 } 169 } 170 171 if(isForbbidenPosition(position)) { 172 continue; 173 } 174 175 const treeOffset = 0.25; 176 // 1.50 numbero magico para posicionar correctamente los arboles con 177 // respecto al terreno 178 position.x -= (widthSegments+treesBorderPadding+1.50)/2; 179 position.y += amplitudeBottom - treeOffset; 180 position.z -= (heightSegments+treesBorderPadding)/2; 181 translationMatrix.makeTranslation(position); 182 treeLogMatrix.identity(); 183 treeLeavesMatrix.identity(); 184 185 let scale = 0.6 + (Math.random()*(logHeight/3)); 186 treeLogMatrix.makeScale(1, scale, 1); 187 treeLogMatrix.premultiply(translationMatrix); 188 189 position.y += scale*logHeight; 190 translationMatrix.makeTranslation(position); 191 treeLeavesMatrix.premultiply(translationMatrix); 192 193 instancedTreeLogs.setMatrixAt(i, treeLogMatrix); 194 instancedTreeLeaves.setMatrixAt(i, treeLeavesMatrix); 195 } 196 197 console.log('Done generating `' + count + '` instances of tree'); 198 return [instancedTreeLogs, instancedTreeLeaves]; 199 } 200 201 202 // La funcion devuelve una geometria de Three.js 203 // width: Ancho del plano 204 // height: Alto del plano 205 // amplitude: Amplitud de la elevacion 206 // widthSegments: Numero de segmentos en el ancho 207 // heightSegments: Numero de segmentos en el alto 208 function elevationGeometry(width, height, amplitude, widthSegments, heightSegments) { 209 console.log('Generating terrain geometry'); 210 let geometry = new THREE.BufferGeometry(); 211 212 const positions = []; 213 const indices = []; 214 const normals = []; 215 const uvs = []; 216 217 let imageData = elevationMap; 218 let data = elevationMapData; 219 220 const quadsPerRow = widthSegments - 1; 221 222 // Recorremos los segmentos horizontales y verticales 223 for (let x = 0; x < widthSegments - 1; x++) { 224 for (let y = 0; y < heightSegments - 1; y++) { 225 // valor del pixel en el mapa de elevacion en la posicion i, j 226 227 let pixel = getPixelXY(imageData, x, y); 228 229 // se utiliza el canal rojo [R, G, B, A]; 230 let z0 = pixel[0] / 255; 231 const z = amplitude * z0; 232 233 // valores de los píxeles de los puntos adyacentes 234 let xPrev, xNext, yPrev, yNext; 235 236 xPrev = (x > 0) ? getPixelXY(imageData, x-1, y)[0]/255 : undefined; 237 xNext = (x < widthSegments-1) ? getPixelXY(imageData, x+1, y)[0]/255 : undefined; 238 239 yPrev = (y > 0) ? getPixelXY(imageData, x, y+1)[0]/255 : undefined; 240 yNext = (y < heightSegments-1) ? getPixelXY(imageData, x, y+1)[0]/255 : undefined; 241 242 // diferencia entre los valores de los píxeles adyacentes en el eje 243 // `x` y en el eje `y` de la imagen en el espacio de la textura 244 let deltaX; 245 if (xPrev == undefined) { 246 deltaX = xNext - z0; 247 } else if (yNext == undefined) { 248 deltaX = xPrev - z0; 249 } else { 250 deltaX = (xNext - xPrev) / 2; 251 } 252 253 let deltaY; 254 if (yPrev == undefined) { 255 deltaY = yNext - z0; 256 } else if (yNext == undefined) { 257 deltaY = yPrev - z0; 258 } else { 259 deltaY = (yNext - yPrev) / 2; 260 } 261 262 // Añadimos los valores de los puntos al array de posiciones 263 positions.push((width * x) / widthSegments - width / 2); 264 positions.push(z); 265 positions.push((height * y) / heightSegments - height / 2); 266 267 // vectores tangentes a la superficie en el eje `x` y en el eje `y` 268 let tanX = new THREE.Vector3(width / widthSegments, deltaX * amplitude, 0); 269 let tanY = new THREE.Vector3(0, deltaY * amplitude, height / heightSegments); 270 271 tanX.normalize(); 272 tanY.normalize(); 273 274 let normal = new THREE.Vector3().crossVectors(tanY, tanX); 275 276 normals.push(normal.x); 277 normals.push(normal.y); 278 normals.push(normal.z); 279 280 uvs.push(x / (widthSegments - 1)); 281 uvs.push(y / (heightSegments - 1)); 282 283 if ((x == widthSegments-2) || (y == heightSegments-2)) continue; 284 285 // Ensamblamos los triangulos 286 indices.push(x + y*quadsPerRow); 287 indices.push(x + 1 + y*quadsPerRow); 288 indices.push(x + 1 + (y+1)*quadsPerRow); 289 290 indices.push(x + y*quadsPerRow); 291 indices.push(x + 1 + (y+1)*quadsPerRow); 292 indices.push(x + (y+1)*quadsPerRow); 293 } 294 } 295 296 geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); 297 geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)); 298 geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2)); 299 geometry.setIndex(indices); 300 301 return geometry; 302 } 303 304 function buildScene() { 305 console.log('Building scene'); 306 307 const width = widthSegments; 308 const height = heightSegments; 309 310 terrainGeometry = elevationGeometry( 311 width, height, 312 amplitude, 313 widthSegments, heightSegments); 314 315 console.log('Applying textures'); 316 terrainMaterial = new THREE.RawShaderMaterial({ 317 uniforms: { 318 dirtSampler: { type: 't', value: textures.tierra.object }, 319 rockSampler: { type: 't', value: textures.roca.object }, 320 grassSampler: { type: 't', value: textures.pasto.object }, 321 scale: { type: 'f', value: 3.0 }, 322 terrainAmplitude: { type: 'f', value: amplitude }, 323 terrainAmplitudeBottom: { type: 'f', value: amplitudeBottom }, 324 worldNormalMatrix: { type: 'm4', value: null }, 325 dirtStepWidth: { type: 'f', value: 0.20 }, 326 rockStepWidth: { type: 'f', value: 0.15 }, 327 }, 328 vertexShader: vertexShader, 329 fragmentShader: fragmentShader, 330 side: THREE.DoubleSide, 331 }); 332 terrainMaterial.needsUpdate = true; 333 334 terrain = new THREE.Mesh(terrainGeometry, terrainMaterial); 335 terrain.position.set(0, amplitudeBottom, 0); 336 scene.add(terrain); 337 338 const waterGeometry = new THREE.PlaneGeometry(width/2, height-1.55); 339 const waterMaterial = new THREE.MeshPhongMaterial( {color: 0x12ABFF, side: THREE.DoubleSide} ); 340 const water = new THREE.Mesh( waterGeometry, waterMaterial ); 341 water.rotateX(Math.PI/2); 342 water.position.set(0, 0.75, -1.00); 343 scene.add(water); 344 345 const [treeLogs, treeLeaves] = createInstancedTrees(250); 346 scene.add(treeLogs); 347 scene.add(treeLeaves); 348 } 349 350 function onTextureLoaded(key, texture) { 351 texture.wrapS = texture.wrapT = THREE.RepeatWrapping; 352 textures[key].object = texture; 353 console.log('Texture `' + key + '` loaded'); 354 } 355 356 function loadTextures(callback) { 357 const loadingManager = new THREE.LoadingManager(); 358 359 loadingManager.onLoad = () => { 360 console.log('All textures loaded'); 361 callback(); 362 }; 363 364 for (const key in textures) { 365 console.log("Loading textures"); 366 const loader = new THREE.TextureLoader(loadingManager); 367 const texture = textures[key]; 368 texture.object = loader.load( 369 texture.url, 370 onTextureLoaded.bind(this, key), 371 null, 372 (error) => { 373 console.error(error); 374 } 375 ); 376 } 377 } 378 379 function createMenu() { 380 const gui = new dat.GUI({ width: 400 }); 381 gui.add(terrainMaterial.uniforms.scale, 'value', 1.00, 5.00).name('Terrain texture scale'); 382 gui.add(terrainMaterial.uniforms.dirtStepWidth, 'value', 0.0, 1.0).name('dirt step width'); 383 gui.add(terrainMaterial.uniforms.rockStepWidth, 'value', 0.10, 0.50).name('rock step width'); 384 } 385 386 function mainLoop() { 387 requestAnimationFrame(mainLoop); 388 renderer.render(scene, camera); 389 } 390 391 function loadMapsData() { 392 console.log("Loading maps data"); 393 394 // Creamos un canvas para poder leer los valores de los píxeles de la textura 395 let canvas = document.createElement('canvas'); 396 let ctx = canvas.getContext('2d'); 397 398 let treesForbiddenMapImage = textures.treeForbiddenMap.object.image; 399 let elevationMapImage = textures.elevationMap.object.image; 400 401 // ambos mapas deben tener el mismo tamaño 402 const imgWidth = widthSegments; 403 const imgHeight = heightSegments; 404 405 canvas.width = imgWidth; 406 canvas.height = imgHeight; 407 408 ctx.drawImage(treesForbiddenMapImage, 0, 0, imgWidth, imgHeight); 409 treesForbiddenMap = ctx.getImageData(0, 0, imgWidth, imgHeight); 410 treesForbiddenMapData = treesForbiddenMap.data; 411 412 ctx.drawImage(elevationMapImage, 0, 0, imgWidth, imgHeight); 413 elevationMap = ctx.getImageData(0, 0, imgWidth, imgHeight); 414 elevationMapData = elevationMap.data 415 416 console.log("All maps data loaded succesfully"); 417 } 418 419 function main() { 420 loadMapsData(); 421 buildScene(); 422 // getTrainTrackShape(); 423 mainLoop(); 424 } 425 426 setupThreeJs(); 427 loadTextures(main);