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