TA159

Notas, resueltos y trabajos practicos de la materia Sistemas Gráficos
Index Commits Files Refs Submodules README LICENSE
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 }