TA159

Notas, resueltos y trabajos practicos de la materia Sistemas Gráficos
Index Commits Files Refs Submodules README LICENSE
commit 2c7a1e0d37ebd1237c89d1f4228fc873e4342840
parent d0156f875221aac6e9ceba896eb73832f53d6f5d
Author: Martin J. Klöckner <mjkloeckner@gmail.com>
Date:   Sat, 29 Jun 2024 00:19:11 -0300

Merge branch `tp`


Diffstat:
Atp/README.md | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/assets/durmientes.jpg | 0
Atp/assets/elevation_map.png | 0
Atp/assets/elevation_map2.png | 0
Atp/assets/elevation_map3.png | 0
Atp/assets/elevation_map_wider_river.png | 0
Atp/assets/madera.jpg | 0
Atp/assets/pared-de-ladrillos.jpg | 0
Atp/assets/pasto.jpg | 0
Atp/assets/roca.jpg | 0
Atp/assets/shaders.js | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/assets/sky_day.jpg | 0
Atp/assets/tierra.jpg | 0
Atp/assets/tierraSeca.jpg | 0
Atp/assets/tree_forbidden_zone_map.png | 0
Atp/assets/tree_forbidden_zone_map_wider_path.png | 0
Atp/assets/treesShaders.js | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/bridge.html | 18++++++++++++++++++
Atp/enunciado/enunciado.pdf | 0
Atp/index.html | 43+++++++++++++++++++++++++++++++++++++++++++
Atp/package.json | 18++++++++++++++++++
Atp/rails.html | 18++++++++++++++++++
Atp/scene.html | 18++++++++++++++++++
Atp/src/bridge.js | 313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/rails.js | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/scene.js | 331+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/standalone/bridge.js | 342+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/standalone/rails.js | 274+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/standalone/terrain.js | 371+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/standalone/track-map.js | 423+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/standalone/train.js | 338+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/standalone/trees.js | 204+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/standalone/tunnel.js | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/terrain.js | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/track-map.js | 357+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/train.js | 311+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/trees.js | 204+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atp/src/tunnel.js | 46++++++++++++++++++++++++++++++++++++++++++++++
Atp/terrain.html | 18++++++++++++++++++
Atp/track-map.html | 18++++++++++++++++++
Atp/train-tree.gv | 21+++++++++++++++++++++
Atp/train-tree.png | 0
Atp/train.html | 18++++++++++++++++++
Atp/trees.html | 18++++++++++++++++++
Atp/tunnel.html | 18++++++++++++++++++
45 files changed, 4681 insertions(+), 0 deletions(-)
diff --git a/tp/README.md b/tp/README.md
@@ -0,0 +1,120 @@
+# Trabajo Practico Sistemas Gráficos
+
+El trabajo practico consiste en implementar una escena en 3D en
+[WebGL](https://www.khronos.org/webgl/) utilizando la librería de JavaScript
+[Three.js](https://threejs.org/)
+
+## Inicio Rápido
+
+Clone el repositorio con el siguiente comando:
+
+```console
+$ git clone https://github.com/mjkloeckner/86.43.git
+```
+
+Navegue al directorio del repositorio clonado y posteriormente a la carpeta del
+trabajo práctico
+
+```console
+$ cd 86.43/tp/
+```
+
+Instale las dependencias
+
+```console
+$ npm i
+```
+
+Finalmente inicialice un servidor Web en el directorio del trabajo práctico y
+abra su navegador de internet preferido en la `url` especificada por `vite`
+
+```console
+$ vite
+```
+
+## Dependencias
+
+* Un navegador que soporte [WebGL](https://get.webgl.org/)
+* [Node.js](https://nodejs.org/)
+* [npm](https://www.npmjs.com/)
+* [vite](https://www.npmjs.com/package/vite)
+
+## Objetivos
+
+* [X] Terreno
+* [X] Arboles
+* [X] Vias de Tren
+    - [X] Crear el terraplen
+    - [X] Crear las vias
+* [ ] Locomotora
+    - [X] Chassis, cabina, ruedas, caldera
+    - [ ] Aplicar textura a las ruedas
+    - [ ] Hacer que las ruedas roten sobre su eje
+* [X] Puente
+    - [X] Base de ladrillos
+    - [X] Estructura de Hierro
+* [X] Túnel
+* [ ] Cámaras
+    - [X] Orbital general
+    - [ ] Fija, locomotora frontal (desde la cabina hacia adelante)
+    - [ ] Fija, locomotora trasera (desde la cabina hacia atrás)
+    - [ ] Fija, con vistas al interior del túnel
+    - [ ] Fija, con vistas al interior del puente
+    - [ ] Primera persona (debe poder moverse sobre el terreno con el teclado y el mouse)
+* [X] Texturas
+* [ ] Iluminación
+    - [ ] Modo noche/día
+* [ ] Posicionar los objetos en la escena final
+    - [X] Mapa de zonas prohibidas para la generación de arboles
+    - [X] Locomotora
+
+## Como se generan los elementos de la escena
+
+### Terreno
+
+Para generar la geometría del terreno se utiliza un mapa de elevación el cual se
+puede encontrar en [`assets/elevationMap.png`](./assets/elevationMap.png), luego
+para la textura del terreno se utiliza 3 texturas diferentes, las cuales
+se utilizan en el terreno de acuerdo a la elevación del mismo.
+
+Para utilizar la misma textura varias veces y evitar que se note la repetición,
+se utiliza la función Ruido de Perlin para obtener valores pseudo-aleatorios.
+
+### Arboles
+
+Los arboles se generan de manera aleatoria en todo el mapa, y se utiliza el mapa
+de elevación para verificar que no caiga en un punto muy bajo o muy alto, como
+puede ser montaña o rió, de acuerdo a un parámetro fijo
+
+### Terraplén y Vías de Tren
+
+Para generar las vías del tren se utiliza la función
+[`ParametricGeometry`](https://threejs.org/docs/index.html?q=param#examples/en/geometries/ParametricGeometry)
+de `three.js`, la cual genera una geometría a partir de una función paramétrica
+que recibe tres parámetros: `u`, `v` y un vector en espacio 3D.
+
+### Locomotora
+
+A continuación se muestra el árbol de dependencias de los objetos que componen
+la locomotora
+
+![Objeto tren: árbol de dependencia](./train-tree.png)
+
+Para realizar cada objeto de la locomotora se utilizan funciones primitivas de `three.js` tales como
+[`BoxGeometry`](https://threejs.org/docs/index.html?q=box#api/en/geometries/BoxGeometry) o
+[`CylinderGeometry`](https://threejs.org/docs/index.html?q=cylin#api/en/geometries/CylinderGeometry),
+las cuales generan cubos y cilindros, respectivamente.
+
+### Puente
+
+### Túnel
+
+### Cámaras
+
+### Texturas
+
+### Iluminación
+
+## Recursos Consultados
+
+* [Documentación de Three.js](https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene)
diff --git a/tp/assets/durmientes.jpg b/tp/assets/durmientes.jpg
Binary files differ.
diff --git a/tp/assets/elevation_map.png b/tp/assets/elevation_map.png
Binary files differ.
diff --git a/tp/assets/elevation_map2.png b/tp/assets/elevation_map2.png
Binary files differ.
diff --git a/tp/assets/elevation_map3.png b/tp/assets/elevation_map3.png
Binary files differ.
diff --git a/tp/assets/elevation_map_wider_river.png b/tp/assets/elevation_map_wider_river.png
Binary files differ.
diff --git a/tp/assets/madera.jpg b/tp/assets/madera.jpg
Binary files differ.
diff --git a/tp/assets/pared-de-ladrillos.jpg b/tp/assets/pared-de-ladrillos.jpg
Binary files differ.
diff --git a/tp/assets/pasto.jpg b/tp/assets/pasto.jpg
Binary files differ.
diff --git a/tp/assets/roca.jpg b/tp/assets/roca.jpg
Binary files differ.
diff --git a/tp/assets/shaders.js b/tp/assets/shaders.js
@@ -0,0 +1,111 @@
+export const vertexShader = `
+    precision highp float;
+
+    // Atributos de los vértices
+    attribute vec3 position; // Posición del vértice
+    attribute vec3 normal;   // Normal del vértice
+    attribute vec2 uv;       // Coordenadas de textura
+
+    // Uniforms
+    uniform mat4 modelMatrix;       // Matriz de transformación del objeto
+    uniform mat4 viewMatrix;        // Matriz de transformación de la cámara
+    uniform mat4 projectionMatrix;  // Matriz de proyección de la cámara
+    uniform mat4 worldNormalMatrix; // Matriz de normales
+
+    // Varying
+    varying vec2  vUv;       // Coordenadas de textura que se pasan al fragment shader
+    varying vec3  vNormal;   // Normal del vértice que se pasa al fragment shader
+    varying vec3  vWorldPos; // Posición del vértice en el espacio  de mundo
+
+    void main() {
+        // Lee la posición del vértice desde los atributos
+        vec3 pos = position;
+
+        // Se calcula la posición final del vértice
+        // Se aplica la transformación del objeto, la de la cámara y la de proyección
+        gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(pos, 1.0);
+
+        // Se pasan las coordenadas de textura al fragment shader
+        vUv = uv;
+        vNormal = normalize(vec3(worldNormalMatrix * vec4(normal, 0.0)));
+        vWorldPos = (modelMatrix * vec4(pos, 1.0)).xyz;
+    }
+`;
+
+export const fragmentShader = `
+    precision mediump float;
+    varying vec2 vUv;
+    varying vec3 vNormal;
+    varying vec3 vWorldPos;
+
+    uniform float scale;
+    uniform float terrainAmplitude;
+    uniform float terrainAmplitudeBottom;
+    uniform float dirtStepWidth;
+    uniform float rockStepWidth;
+
+    uniform sampler2D dirtSampler;
+    uniform sampler2D rockSampler;
+    uniform sampler2D grassSampler;
+
+    float normalize(float inputValue, float minValue, float maxValue) {
+        return (inputValue - minValue) / (maxValue - minValue);
+    }
+
+    void main(void) {
+        vec2 uv = vUv*8.0;
+        vec2 uv2 = vUv*scale;
+
+        float verticallity = 1.0-max(0.0,vNormal.y);
+        float flatness = 1.0-verticallity;
+        float heightFactor = vWorldPos.y - terrainAmplitudeBottom;
+        float heightFactorNormalized = normalize(heightFactor, 0.0, terrainAmplitude);
+
+        vec3 grass = texture2D(grassSampler, uv).xyz;
+        vec3 dirt  = texture2D(dirtSampler, uv*4.0).xyz;
+        vec3 rock  = texture2D(rockSampler, uv).xyz;
+
+        // muestreo de pasto a diferentes escalas, luego se combina con \`mix()\`
+        vec3 grass1 = texture2D(grassSampler, uv2*1.00).xyz;
+        vec3 grass2 = texture2D(grassSampler, uv2*3.13).xyz;
+        vec3 grass3 = texture2D(grassSampler, uv2*2.37).xyz;
+        vec3 colorGrass = mix(mix(grass1,grass2,0.5),grass3,0.3);
+
+        // lo mismo para la textura de tierra
+        vec3 dirt1 = texture2D(dirtSampler, uv2*3.77).xyz;
+        vec3 dirt2 = texture2D(dirtSampler, uv2*1.58).xyz;
+        vec3 dirt3 = texture2D(dirtSampler, uv2*1.00).xyz;
+        vec3 colorDirt = mix(mix(dirt1, dirt2, 0.5), dirt3, 0.3);
+
+        // lo mismo para la textura de roca
+        vec3 rock1 = texture2D(rockSampler,uv2*0.40).xyz;
+        vec3 rock2 = texture2D(rockSampler,uv2*2.38).xyz;
+        vec3 rock3 = texture2D(rockSampler,uv2*3.08).xyz;
+        vec3 colorRock = mix(mix(rock1, rock2, 0.5), rock3,0.5);
+
+        float u = heightFactorNormalized;
+
+        // float pi = 3.141592654;
+        // float grassFactor = sin(pi*u);
+        // float dirtFactor  = abs(sin(2.0*pi));
+        // float rockFactor  = clamp(cos(2.0*pi*u), 0.0, 1.0);
+
+        float width2 = rockStepWidth;
+        float rockFactor = 2.00 - smoothstep(0.0, width2, u)
+                                - smoothstep(1.0, 1.00 - width2, u);
+
+        float width = dirtStepWidth;
+        float s1 = smoothstep(0.00, width, u);
+        float s2 = smoothstep(width, width*2.0, u);
+        float s3 = smoothstep(0.50, 0.50 + width, u);
+        float s4 = smoothstep(0.50 + width, 0.50 + width*2.0, u);
+        float dirtFactor = (s1 - s2) + (s3 - s4);
+
+        float grassFactor = smoothstep(0.0, 0.35, u) - smoothstep(0.35, 1.00, u);
+
+        vec3 colorDirtGrass = mix(colorDirt, colorGrass, grassFactor);
+        vec3 colorDirtGrassDirt = mix(colorDirtGrass, colorDirt, dirtFactor);
+        vec3 color = mix(colorDirtGrassDirt, colorRock, rockFactor);
+
+        gl_FragColor = vec4(color, 1.0);
+    }`;
diff --git a/tp/assets/sky_day.jpg b/tp/assets/sky_day.jpg
Binary files differ.
diff --git a/tp/assets/tierra.jpg b/tp/assets/tierra.jpg
Binary files differ.
diff --git a/tp/assets/tierraSeca.jpg b/tp/assets/tierraSeca.jpg
Binary files differ.
diff --git a/tp/assets/tree_forbidden_zone_map.png b/tp/assets/tree_forbidden_zone_map.png
Binary files differ.
diff --git a/tp/assets/tree_forbidden_zone_map_wider_path.png b/tp/assets/tree_forbidden_zone_map_wider_path.png
Binary files differ.
diff --git a/tp/assets/treesShaders.js b/tp/assets/treesShaders.js
@@ -0,0 +1,166 @@
+export const vertexShader = `
+    precision highp float;
+
+    attribute vec3 position;
+    attribute vec2 uv;
+
+    uniform mat4 modelMatrix;      // Matriz de transformación del objeto
+    uniform mat4 viewMatrix;       // Matriz de transformación de la cámara
+    uniform mat4 projectionMatrix; // Matriz de proyección de la cámara
+
+    varying vec2 vUv;
+
+    void main() {
+        vec3 pos = position;
+        gl_Position = projectionMatrix*viewMatrix*modelMatrix* vec4(pos, 1.0);
+        vUv = uv;
+    }`;
+
+export const fragmentShader = `
+    precision mediump float;
+
+    uniform float scale1;
+    uniform float mask1low;
+    uniform float mask1high;
+    uniform float mask2low;
+    uniform float mask2high;
+    uniform sampler2D tierraSampler;
+    uniform sampler2D rocaSampler;
+    uniform sampler2D pastoSampler;
+
+    varying vec2 vUv;
+
+    // Perlin Noise
+    vec3 mod289(vec3 x){
+        return x - floor(x * (1.0 / 289.0)) * 289.0;
+    }
+
+    vec4 mod289(vec4 x){
+        return x - floor(x * (1.0 / 289.0)) * 289.0;
+    }
+
+    vec4 permute(vec4 x){
+        return mod289(((x * 34.0) + 1.0) * x);
+    }
+
+    vec4 taylorInvSqrt(vec4 r){
+        return 1.79284291400159 - 0.85373472095314 * r;
+    }
+
+    vec3 fade(vec3 t) {
+        return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
+    }
+
+    // Classic Perlin noise
+    float cnoise(vec3 P){
+        vec3 Pi0 = floor(P);
+        vec3 Pi1 = Pi0 + vec3(1.0);
+        Pi0 = mod289(Pi0);
+        Pi1 = mod289(Pi1);
+        vec3 Pf0 = fract(P);
+        vec3 Pf1 = Pf0 - vec3(1.0);
+        vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
+        vec4 iy = vec4(Pi0.yy, Pi1.yy);
+        vec4 iz0 = Pi0.zzzz;
+        vec4 iz1 = Pi1.zzzz;
+
+        vec4 ixy = permute(permute(ix) + iy);
+        vec4 ixy0 = permute(ixy + iz0);
+        vec4 ixy1 = permute(ixy + iz1);
+
+        vec4 gx0 = ixy0 * (1.0 / 7.0);
+        vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5;
+        gx0 = fract(gx0);
+        vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
+        vec4 sz0 = step(gz0, vec4(0.0));
+        gx0 -= sz0 * (step(0.0, gx0) - 0.5);
+        gy0 -= sz0 * (step(0.0, gy0) - 0.5);
+
+        vec4 gx1 = ixy1 * (1.0 / 7.0);
+        vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5;
+        gx1 = fract(gx1);
+        vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
+        vec4 sz1 = step(gz1, vec4(0.0));
+        gx1 -= sz1 * (step(0.0, gx1) - 0.5);
+        gy1 -= sz1 * (step(0.0, gy1) - 0.5);
+
+        vec3 g000 = vec3(gx0.x, gy0.x, gz0.x);
+        vec3 g100 = vec3(gx0.y, gy0.y, gz0.y);
+        vec3 g010 = vec3(gx0.z, gy0.z, gz0.z);
+        vec3 g110 = vec3(gx0.w, gy0.w, gz0.w);
+        vec3 g001 = vec3(gx1.x, gy1.x, gz1.x);
+        vec3 g101 = vec3(gx1.y, gy1.y, gz1.y);
+        vec3 g011 = vec3(gx1.z, gy1.z, gz1.z);
+        vec3 g111 = vec3(gx1.w, gy1.w, gz1.w);
+
+        vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000),
+                                    dot(g010, g010),
+                                    dot(g100, g100),
+                                    dot(g110, g110)));
+        g000 *= norm0.x;
+        g010 *= norm0.y;
+        g100 *= norm0.z;
+        g110 *= norm0.w;
+        vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001),
+                                    dot(g011, g011),
+                                    dot(g101, g101),
+                                    dot(g111, g111)));
+        g001 *= norm1.x;
+        g011 *= norm1.y;
+        g101 *= norm1.z;
+        g111 *= norm1.w;
+
+        float n000 = dot(g000, Pf0);
+        float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
+        float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
+        float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
+        float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
+        float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
+        float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
+        float n111 = dot(g111, Pf1);
+
+        vec3 fade_xyz = fade(Pf0);
+        vec4 n_z = mix(vec4(n000, n100, n010, n110),
+                        vec4(n001, n101, n011, n111),
+                        fade_xyz.z);
+
+        vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
+        float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); 
+        return 2.2 * n_xyz;
+    }
+
+    void main(void) {
+        vec2 uv2=vUv*scale1;
+
+        // se mezcla la textura 'pasto' a diferentes escalas
+        vec3 pasto1 = texture2D(pastoSampler, uv2 * 1.0).xyz;
+        vec3 pasto2 = texture2D(pastoSampler, uv2 * 3.13).xyz;
+        vec3 pasto3 = texture2D(pastoSampler, uv2 * 2.37).xyz;
+        vec3 colorPasto = mix(mix(pasto1, pasto2, 0.5), pasto3, 0.3);
+
+        // lo mismo para la textura 'tierra'
+        vec3 tierra1 = texture2D(tierraSampler, uv2*3.77).xyz;
+        vec3 tierra2 = texture2D(tierraSampler, uv2*1.58).xyz;
+        vec3 colorTierra = mix(tierra1, tierra2, 0.5);
+
+        // lo mismo para la textura 'roca'
+        vec3 roca1 = texture2D(rocaSampler, uv2).xyz;
+        vec3 roca2 = texture2D(rocaSampler, uv2*2.38).xyz;
+        vec3 colorRoca = mix(roca1, roca2, 0.5);
+
+        float noise1 = cnoise(uv2.xyx*8.23+23.11);
+        float noise2 = cnoise(uv2.xyx*11.77+9.45);
+        float noise3 = cnoise(uv2.xyx*14.8+21.2);
+        float mask1 = mix(mix(noise1, noise2, 0.5), noise3, 0.3);
+        mask1 = smoothstep(mask1low, mask1high, mask1);
+
+        float noise4 = cnoise(uv2.xyx*8.23*scale1);
+        float noise5 = cnoise(uv2.xyx*11.77*scale1);
+        float noise6 = cnoise(uv2.xyx*14.8*scale1);
+        float mask2 = mix(mix(noise4, noise5, 0.5), noise6, 0.3);
+        mask2 = smoothstep(mask2low, mask2high, mask2);
+        vec3 colorTierraRoca = mix(colorTierra, colorRoca, mask1);
+        vec3 color = mix(colorPasto, colorTierraRoca, mask2);
+
+        gl_FragColor = vec4(color, 1.0);
+    }`;
diff --git a/tp/bridge.html b/tp/bridge.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>Trabajo Practico Sistemas Graficos | Martin Klöckner</title>
+        <link rel="icon" href="data:,"> <!-- Do not request favicon -->
+        <style>
+            html, body, #mainContainer {
+                padding: 0;
+                margin: 0;
+                height: 100%;
+        </style>
+    </head>
+    <body>
+        <div id="mainContainer"></div>
+        <script type="module" src="/src/standalone/bridge.js"></script>
+    </body>
+</html>
diff --git a/tp/enunciado/enunciado.pdf b/tp/enunciado/enunciado.pdf
Binary files differ.
diff --git a/tp/index.html b/tp/index.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>Trabajo Practico Sistemas Graficos | Martin Klöckner</title>
+        <link rel="icon" href="data:,"> <!-- Do not request favicon -->
+        <style>
+            html, body, #mainContainer {
+                padding: 0;
+                margin: 0;
+                height: 100%;
+                font-family: Baskerville, "Times New Roman", serif;
+            }
+            body {
+                margin-left: auto;
+                margin-right: auto;
+                width: 800px;
+                padding: 1em;
+                font-size: 14px;
+            }
+        </style>
+    </head>
+    <body>
+        <h1>Trabajo Practico: Escena 3D en WebGL</h1>
+        <p>Sistemas Graficos (86.43) - FIUBA</p>
+        <p style="margin-top: -0.5em;">Martin Klöckner - <a href="mailto:mkloeckner@fi.uba.ar">mkloeckner@fi.uba.ar</a></p>
+        <p style="margin-top: -0.5em;">Repositorio de Github: <a href="https://github.com/mjkloeckner/86.43-tp">https://github.com/mjkloeckner/86.43-tp</a></p>
+        <h2>Componentes de la Escena</h2>
+        <ul>
+            <li><a href="terrain.html">Terreno</a></li>
+            <li><a href="trees.html">Arboles</a></li>
+            <li><a href="train.html">Tren</a></li>
+            <li><a href="rails.html">Vias y Terraplen</a></li>
+            <li><a href="tunnel.html">Tunel</a></li>
+            <li><a href="bridge.html">Puente</a></li>
+            <li><a href="track-map.html">Mapa de Posiciones en las que no se Deben Generar Arboles</a></li>
+        </ul>
+        <h2>Escena Final</h2>
+        <ul>
+            <li><a href="scene.html">Escena Final</a></li>
+        </ul>
+    </body>
+</html>
diff --git a/tp/package.json b/tp/package.json
@@ -0,0 +1,18 @@
+{
+  "name": "Trabajo Práctico",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "MIT",
+  "dependencies": {
+    "dat.gui": "^0.7.9",
+    "three": "^0.162.0"
+  },
+  "devDependencies": {
+    "vite": "^5.1.4"
+  }
+}
diff --git a/tp/rails.html b/tp/rails.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>Trabajo Practico Sistemas Graficos | Martin Klöckner</title>
+        <link rel="icon" href="data:,"> <!-- Do not request favicon -->
+        <style>
+            html, body, #mainContainer {
+                padding: 0;
+                margin: 0;
+                height: 100%;
+        </style>
+    </head>
+    <body>
+        <div id="mainContainer"></div>
+        <script type="module" src="/src/standalone/rails.js"></script>
+    </body>
+</html>
diff --git a/tp/scene.html b/tp/scene.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>Trabajo Practico Sistemas Graficos | Martin Klöckner</title>
+        <link rel="icon" href="data:,"> <!-- Do not request favicon -->
+        <style>
+            html, body, #mainContainer {
+                padding: 0;
+                margin: 0;
+                height: 100%;
+        </style>
+    </head>
+    <body>
+        <div id="mainContainer"></div>
+        <script type="module" src="/src/scene.js"></script>
+    </body>
+</html>
diff --git a/tp/src/bridge.js b/tp/src/bridge.js
@@ -0,0 +1,313 @@
+import * as THREE from 'three';
+import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
+
+const textures = {
+    tierra:     { url: '/assets/tierraSeca.jpg', object: null },
+    ladrillos:  { url: '/assets/pared-de-ladrillos.jpg', object: null },
+};
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+// const arcRadius    = 3;
+// const arcCount     = 1;
+// const columnHeight = 0;
+// const columnWidth  = 1.00;
+// const arcWidth     = arcRadius*2;
+// const startPadding = 12;
+// const endPadding   = startPadding;
+// const bridgeHeight        = columnHeight+arcRadius+topPadding;
+
+const topPadding   = 0.25;
+const bridgeWallThickness = 2.5;
+const bridgeWidth   = 10;
+const roadwayHeight = 0.65;
+
+function generateBridgeWallGeometry(
+        arcCount=2, arcRadius=3, columnWidth=1, columnHeight=0, padding=10) {
+
+    const arcWidth = arcRadius*2;
+    const startPadding = padding;
+    const endPadding = padding;
+    const bridgeLen = arcCount*(columnWidth+arcWidth)+columnWidth+startPadding+endPadding;
+    const bridgeHeight = columnHeight+arcRadius+topPadding;
+    const path = new THREE.Path();
+
+    // generate the arcs
+    for(let i = 1; i <= arcCount; ++i) {
+        path.lineTo(startPadding+i*columnWidth+((i-1)*arcWidth), 0);
+        path.moveTo(startPadding+i*columnWidth+((i-1)*arcWidth), 0);
+        path.lineTo(startPadding+i*columnWidth+((i-1)*arcWidth), columnHeight);
+        path.arc(arcRadius, 0, arcRadius, Math.PI, 0, true)
+        path.moveTo(startPadding+i*(columnWidth+arcWidth), 0);
+        path.lineTo(startPadding+i*(columnWidth+arcWidth), 0);
+    }
+
+    // no we close the curve
+    path.lineTo(bridgeLen, 0);
+    path.lineTo(bridgeLen, bridgeHeight);
+
+    path.lineTo(0, bridgeHeight);
+    path.lineTo(0, 0);
+
+    /*
+    // muestra la curva utilizada para la extrusión
+    const geometry = new THREE.BufferGeometry().setFromPoints(points);
+    const lineMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
+    const curveObject = new THREE.Line(geometry, lineMaterial);
+    scene.add(curveObject);
+    */
+
+    const points = path.getPoints();
+    const shape = new THREE.Shape(points);
+
+    const extrudeSettings = {
+        curveSegments: 24,
+        steps: 50,
+        depth: bridgeWallThickness,
+        bevelEnabled: false
+    };
+
+    const bridgeWallGeometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
+    bridgeWallGeometry.translate(-bridgeLen/2, 0, -bridgeWallThickness/2);
+    return bridgeWallGeometry;
+}
+
+function generateBridgeCage(squaresCount=3, squareTubeRadius=0.15) {
+    const squareLen = bridgeWidth - 0.25;
+    const bridgeCageLen  = squaresCount * squareLen;
+
+    let geometries = []
+
+    let cylinderBase, cylinderCorner, cylinderCrossbar;
+    for(let square = 0; square < squaresCount; ++square) {
+        // 0 -> 00
+        // 1 -> 01
+        // 2 -> 10
+        // 3 -> 11
+        for(let i = 0; i < 4; ++i) {
+            cylinderBase = new THREE.CylinderGeometry(
+                squareTubeRadius, squareTubeRadius, squareLen);
+
+            cylinderCorner = cylinderBase.clone();
+
+            const squareHypotenuse = Math.sqrt(2*squareLen*squareLen);
+            cylinderCrossbar = new THREE.CylinderGeometry(
+                squareTubeRadius, squareTubeRadius, squareHypotenuse);
+
+            if((i % 2) == 0) {
+                cylinderBase.rotateZ(Math.PI/2);
+                cylinderBase.translate(
+                    0,
+                    square*(squareLen),
+                    ((-1)**(i>>1))*squareLen/2);
+
+                cylinderCrossbar.rotateZ((-1)**((i>>1))*Math.PI/4);
+                cylinderCrossbar.translate(
+                    0,
+                    square*(squareLen)+(squareLen/2),
+                    ((-1)**(i>>1))*squareLen/2);
+
+                cylinderCorner.translate(
+                    ((-1)**(i>>1))*squareLen/2,
+                    square*(squareLen)+(squareLen/2),
+                    ((-1)**(i&1))*squareLen/2);
+            } else {
+                cylinderBase.rotateX(Math.PI/2);
+                cylinderBase.translate(
+                    ((-1)**(i>>1))*squareLen/2,
+                    square*(squareLen),
+                    0);
+
+                cylinderCrossbar.rotateX((-1)**((i>>1))*Math.PI/4);
+                cylinderCrossbar.translate(
+                    ((-1)**(i>>1))*squareLen/2,
+                    square*(squareLen)+(squareLen/2),
+                    0);
+
+                cylinderCorner.translate(
+                    ((-1)**(i>>1))*squareLen/2,
+                    square*(squareLen)+(squareLen/2),
+                    ((-1)**(i&1))*squareLen/2);
+            }
+            geometries.push(cylinderBase);
+            geometries.push(cylinderCrossbar);
+            geometries.push(cylinderCorner);
+        }
+
+        // agregamos un cuadrado mas para 'cerrar' la 'jaula'
+        if((square + 1) == squaresCount) {
+            for(let i = 0; i < 4; ++i) {
+                cylinderBase = new THREE.CylinderGeometry(
+                    squareTubeRadius, squareTubeRadius, squareLen);
+
+                if((i % 2) == 0) {
+                    cylinderBase.rotateZ(Math.PI/2);
+                    cylinderBase.translate(
+                        0,
+                        (square+1)*(squareLen),
+                        ((-1)**(i>>1))*squareLen/2);
+                } else {
+                    cylinderBase.rotateX(Math.PI/2);
+                    cylinderBase.translate(
+                        ((-1)**(i>>1))*squareLen/2,
+                        (square+1)*(squareLen), 0);
+                }
+                geometries.push(cylinderBase);
+            }
+        }
+    }
+
+    const bridgeCage = mergeGeometries(geometries);
+    bridgeCage.rotateZ(Math.PI/2);
+    bridgeCage.translate(bridgeCageLen/2, squareLen/2, 0);
+    return bridgeCage;
+}
+
+export function generateBridge(arcCount=1, arcRadius=3,
+    columnWidth=0, columnHeight=0, padding=10, squaresCount=0, squareLen=1) {
+
+    const arcWidth     = arcRadius*2;
+    const startPadding = padding;
+    const endPadding   = padding;
+    const bridgeHeight = columnHeight+arcRadius+topPadding;
+    const bridgeLen    = arcCount*(columnWidth+arcWidth)+columnWidth+startPadding+endPadding;
+    const squareTubeRadius = 0.30;
+
+    const bridge       = new THREE.Object3D();
+
+    const leftWallGeometry = generateBridgeWallGeometry(
+        arcCount, arcRadius, columnWidth, columnHeight, padding);
+
+    // const leftWallGeometry = generateBridgeWallGeometry();
+    leftWallGeometry.translate(0, 0, -bridgeWidth/2);
+
+    const rightWallGeometry = generateBridgeWallGeometry(
+        arcCount, arcRadius, columnWidth, columnHeight, padding);
+
+    // const rightWallGeometry = generateBridgeWallGeometry();
+    rightWallGeometry.translate(0, 0, bridgeWidth/2)
+
+    const bridgeColumnsGeometry = mergeGeometries([leftWallGeometry, rightWallGeometry]);
+
+    const bridgeRoadwayGeometry = new THREE.BoxGeometry(
+        bridgeLen, roadwayHeight, bridgeWidth+bridgeWallThickness,
+    );
+
+    bridgeRoadwayGeometry.translate(0, bridgeHeight+roadwayHeight/2, 0);
+
+    textures.ladrillos.object.wrapS = THREE.RepeatWrapping;
+    textures.ladrillos.object.wrapT = THREE.RepeatWrapping;
+    textures.ladrillos.object.repeat.set(0.75*0.15, 0.75*0.35);
+    textures.ladrillos.object.anisotropy = 16;
+
+    const bridgeMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        map: textures.ladrillos.object
+    });
+
+    /*
+    textures.ladrillos2.object.wrapS = THREE.RepeatWrapping;
+    textures.ladrillos2.object.wrapT = THREE.RepeatWrapping;
+    textures.ladrillos2.object.repeat.set(0.75*5, 0.75*0.75);
+    textures.ladrillos2.object.anisotropy = 16;
+
+    const roadwayMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        map: textures.ladrillos2.object
+        // color: 0xFF0000
+    });
+
+    const bridgeRoadway = new THREE.Mesh(bridgeRoadwayGeometry, roadwayMaterial);
+    scene.add(bridgeRoadway);
+    */
+
+    const bridgeColumns = new THREE.Mesh(bridgeColumnsGeometry, bridgeMaterial);
+    bridge.add(bridgeColumns);
+
+    // para reutilizar la textura de ladrillos usada en los arcos se escalan las
+    // coordenadas uv de la geometria de la parte superior
+    let uvs = bridgeRoadwayGeometry.attributes.uv.array;
+    for (let i = 0, len = uvs.length; i < len; i++) {
+        uvs[i] = (i % 2) ? uvs[i]*2.50 : uvs[i]*30.0;
+    }
+
+    const bridgeRoadway = new THREE.Mesh(bridgeRoadwayGeometry, bridgeMaterial);
+    bridge.add(bridgeRoadway);
+
+    const cageGeometry = generateBridgeCage(squaresCount)
+    cageGeometry.translate(0, bridgeHeight+roadwayHeight-squareTubeRadius*2, 0);
+
+    const cageMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        color: 0xFFFFFF
+    });
+
+    const bridgeCage = new THREE.Mesh(cageGeometry, cageMaterial);
+    bridge.add(bridgeCage);
+
+    const roadwayFloorGeometry = new THREE.PlaneGeometry(
+        bridgeWidth+bridgeWallThickness,
+        bridgeLen);
+
+    roadwayFloorGeometry.rotateZ(Math.PI/2)
+    roadwayFloorGeometry.rotateX(Math.PI/2)
+    roadwayFloorGeometry.translate(0, bridgeHeight+roadwayHeight, 0)
+
+    textures.tierra.object.wrapS = THREE.MirroredRepeatWrapping;
+    textures.tierra.object.wrapT = THREE.MirroredRepeatWrapping;
+    textures.tierra.object.repeat.set(1, 5);
+    textures.tierra.object.anisotropy = 16;
+
+    const roadwayFloorMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        map: textures.tierra.object
+    });
+
+    const roadwayFloor = new THREE.Mesh(roadwayFloorGeometry, roadwayFloorMaterial);
+    bridge.add(roadwayFloor)
+    return bridge;
+}
+
+function main() {
+}
+
+loadTextures(main);
diff --git a/tp/src/rails.js b/tp/src/rails.js
@@ -0,0 +1,208 @@
+import * as THREE from 'three';
+
+import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js';
+import { ParametricGeometries } from 'three/examples/jsm/geometries/ParametricGeometries.js';
+import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
+
+let railsPath;
+
+export const railsFoundationShape = new THREE.CatmullRomCurve3([
+    new THREE.Vector3( -2.00, 0.00, 0.00),
+    new THREE.Vector3( -1.00, 0.00, 0.50),
+    new THREE.Vector3(  0.00, 0.00, 0.55),
+    new THREE.Vector3(  1.00, 0.00, 0.50),
+    new THREE.Vector3(  2.00, 0.00, 0.00),
+], false);
+
+
+const textures = {
+    tierra:     { url: '/assets/tierra.jpg', object: null },
+    roca:       { url: '/assets/roca.jpg', object: null },
+    pasto:      { url: '/assets/pasto.jpg', object: null },
+    durmientes: { url: '/assets/durmientes.jpg', object: null },
+};
+
+export function getRailsPathPosAt(t) {
+    if(railsPath == undefined) {
+        console.log("railsPath is undefined");
+    }
+    return railsPath.getPointAt(t);
+}
+
+function parametricRailsFoundationFunction(u, v, target) {
+    const rotMatrix = new THREE.Matrix4();
+    const translationMatrix = new THREE.Matrix4();
+    const levelMatrix = new THREE.Matrix4();
+
+    let railsPathPos = railsPath.getPointAt(v);
+    let railsFoundationShapePos = railsFoundationShape.getPointAt(u);
+    // TODO: make `railsFoundationShape` smaller and remove this multiplication
+    railsFoundationShapePos.multiplyScalar(0.5);
+
+    let tangente = new THREE.Vector3();
+    let binormal = new THREE.Vector3();
+    let normal = new THREE.Vector3();
+
+    tangente = railsPath.getTangent(v);
+
+    tangente.normalize();
+    binormal = new THREE.Vector3(0, 1, 0);
+    normal.crossVectors(tangente, binormal);
+
+    translationMatrix.makeTranslation(railsPathPos);
+
+    rotMatrix.identity();
+    levelMatrix.identity();
+
+    levelMatrix.makeTranslation(railsPathPos);
+    rotMatrix.makeBasis(normal, tangente, binormal);
+    levelMatrix.multiply(rotMatrix);
+    railsFoundationShapePos.applyMatrix4(levelMatrix);
+    
+    const x = railsFoundationShapePos.x;
+    const y = railsFoundationShapePos.y;
+    const z = railsFoundationShapePos.z;
+    target.set(x, y, z);
+}
+
+// devuelve la geometria del terraplen de la via
+export function buildRailsFoundationGeometry() {
+    /*
+    // show rails foundation shape
+    const points = railsFoundationShape.getPoints(50);
+    const geometry = new THREE.BufferGeometry().setFromPoints(points);
+    const lineMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
+    const curveObject = new THREE.Line(geometry, lineMaterial);
+    scene.add(curveObject);
+    */
+
+    const pGeometry = new ParametricGeometry(
+        parametricRailsFoundationFunction, 250, 250);
+    
+    return pGeometry;
+}
+
+// `position` es de tipo `THREE.Vector3` y representa la translacion de la
+// forma del rail con respecto al origen del sist. de coordenadas de modelado
+function getParametricRailsFunction(railsRadius = 0.50, position) {
+    return function parametricRails(u, v, target) {
+        const rotMatrix = new THREE.Matrix4();
+        const translationMatrix = new THREE.Matrix4();
+        const levelMatrix = new THREE.Matrix4();
+
+        let railsShape = new THREE.Vector3();
+
+        let railsPathPos = railsPath.getPointAt(v);
+        let railsShapePos = new THREE.Vector3(
+            Math.cos(u*6.28) + position.x,
+            position.y,
+            Math.sin(u*6.28) + position.z);
+
+        railsShapePos.multiplyScalar(0.1*railsRadius);
+
+        let tangente = new THREE.Vector3();
+        let binormal = new THREE.Vector3();
+        let normal = new THREE.Vector3();
+
+        // https://threejs.org/docs/index.html?q=curve#api/en/extras/core/Curve.getTangent
+        tangente = railsPath.getTangentAt(v);
+        binormal = new THREE.Vector3(0, 1, 0);
+        normal.crossVectors(tangente, binormal);
+
+        translationMatrix.makeTranslation(railsPathPos);
+
+        rotMatrix.identity();
+        levelMatrix.identity();
+
+        levelMatrix.makeTranslation(railsPathPos);
+        rotMatrix.makeBasis(normal, tangente, binormal);
+        levelMatrix.multiply(rotMatrix);
+        railsShapePos.applyMatrix4(levelMatrix);
+        
+        const x = railsShapePos.x;
+        const y = railsShapePos.y;
+        const z = railsShapePos.z;
+        target.set(x, y, z);
+    }
+}
+
+// devuelve la geometria de los rieles
+export function buildRailsGeometry(railsRadius = 0.35) {
+    let railsGeometries = [];
+
+    const leftRailGeometryFunction  = getParametricRailsFunction(railsRadius,
+        new THREE.Vector3( 6, 0, railsRadius+8));
+
+    const rightRailGeometryFunction = getParametricRailsFunction(railsRadius,
+        new THREE.Vector3(-6, 0, railsRadius+8));
+
+    const leftRailGeometry  = new ParametricGeometry(leftRailGeometryFunction, 100, 500);
+    const rightRailGeometry = new ParametricGeometry(rightRailGeometryFunction, 100, 500);
+
+    railsGeometries.push(leftRailGeometry);
+    railsGeometries.push(rightRailGeometry);
+
+    const railsGeometry = mergeGeometries(railsGeometries);
+    return railsGeometry;
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+function main() {
+    railsPath = new THREE.CatmullRomCurve3([
+        // bridge1 side
+        new THREE.Vector3(  0, 0, 32),
+        new THREE.Vector3( 28, 0, 32),
+
+        new THREE.Vector3( 28, 0, 0),
+
+        // bridge2 side
+        new THREE.Vector3(  5, 0, -37),
+        new THREE.Vector3(-35, 0, -30),
+        // new THREE.Vector3(-20, 0, -10),
+
+        new THREE.Vector3(-10, 0, 0),
+    ], true, 'catmullrom', 0.75);
+
+    /*
+    // muestra la curva utilizada para el camino de `rails`
+    const railsPathPoints = railsPath.getPoints(50);
+    const railsPathGeometry = new THREE.BufferGeometry().setFromPoints(railsPathPoints);
+    const railsPathMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
+    const railsPathMesh = new THREE.Line(railsPathGeometry, railsPathMaterial);
+    scene.add(railsPathMesh);
+    */
+
+    // buildRailsFoundation();
+    // buildRails();
+}
+
+// setupThreeJs();
+loadTextures(main);
diff --git a/tp/src/scene.js b/tp/src/scene.js
@@ -0,0 +1,331 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { vertexShader, fragmentShader } from '/assets/shaders.js';
+
+import { generateTunnelGeometry } from '/src/tunnel.js';
+import { createInstancedTrees } from '/src/track-map.js';
+import { elevationGeometry } from '/src/terrain.js';
+import {
+    getRailsPathPosAt,
+    buildRailsGeometry,
+    buildRailsFoundationGeometry
+} from '/src/rails.js';
+import { buildTrain } from '/src/train.js';
+import { generateBridge } from '/src/bridge.js';
+import { updateTrainCrankPosition } from '/src/train.js';
+
+let scene, camera, renderer, container, terrainMaterial, terrainGeometry, terrain, time;
+let treesForbiddenMapData, treesForbiddenMap, elevationMap, elevationMapData;
+
+// actualizar la variable global `amplitude` de '/src/track-map/'
+const widthSegments   = 150;
+const heightSegments  = 150;
+const amplitude       = 10;
+const amplitudeBottom = -2.10; // terrain offset
+
+const textures = {
+    sky:              { url: '/assets/sky_day.jpg', object: null },
+    roca:             { url: '/assets/roca.jpg', object: null },
+    pasto:            { url: '/assets/pasto.jpg', object: null },
+    tierra:           { url: '/assets/tierra.jpg', object: null },
+    madera:           { url: '/assets/madera.jpg', object: null },
+    durmientes:       { url: '/assets/durmientes.jpg', object: null },
+    elevationMap:     { url: '/assets/elevation_map_wider_river.png', object: null },
+    treeForbiddenMap: { url: '/assets/tree_forbidden_zone_map_wider_path.png', object: null }
+};
+
+let settings = {
+    animationEnable: false,
+    showTrain: false
+};
+
+function onResize() {
+    camera.aspect = container.offsetWidth / container.offsetHeight;
+    camera.updateProjectionMatrix();
+    renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+    scene = new THREE.Scene();
+    container = document.getElementById('mainContainer');
+
+    renderer = new THREE.WebGLRenderer();
+    // renderer.setClearColor(0x606060);
+    container.appendChild(renderer.domElement);
+
+    camera = new THREE.PerspectiveCamera(
+        35, window.innerWidth / window.innerHeight, 0.1, 1000);
+
+    camera.position.set(-50, 60, 50);
+    camera.lookAt(0, 0, 0);
+
+    const controls = new OrbitControls(camera, renderer.domElement);
+
+    const ambientLight = new THREE.AmbientLight(0xFFFFFF);
+    scene.add(ambientLight);
+
+    const hemisphereLight = new THREE.HemisphereLight(0xFFFFFF, 0x000000, 0.25);
+    scene.add(hemisphereLight);
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+    directionalLight.position.set(100, 100, 100);
+    scene.add(directionalLight);
+
+    const helper = new THREE.HemisphereLightHelper(hemisphereLight, 5);
+    // scene.add(helper) ;
+
+    const gridHelper = new THREE.GridHelper(200, 200);
+    // scene.add(gridHelper);
+
+    const axesHelper = new THREE.AxesHelper(5);
+    // scene.add(axesHelper);
+
+    window.addEventListener('resize', onResize);
+    onResize();
+
+    textures.sky.object.mapping = THREE.EquirectangularRefractionMapping;
+    scene.background = textures.sky.object;
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+function buildBridge() {
+    // const bridge1 = generateBridge();
+    // const bridge2 = generateBridge();
+
+    // (arcCount, arcRadius, columnWidth, columnHeight, padding, squaresCount, squareLen)
+    const bridge1 = generateBridge(1, 3, 0, 0, 10, 2, 2);
+    const bridge2 = generateBridge(2, 2, 1, 0, 15, 3, 2);
+
+    bridge1.scale.set(0.5, 0.5, 0.5);
+    bridge1.position.set(16, -0.75, 36);
+    // bridge1.rotateY(-Math.PI*0.118);
+
+    bridge2.scale.set(0.5, 0.5, 0.5);
+    bridge2.position.set(-14, 0, -41);
+    // bridge2.rotateY(-Math.PI*0.118);
+
+    scene.add(bridge1);
+    scene.add(bridge2);
+}
+
+let train;
+
+// loco -> locomotora/locomotive
+function buildLoco() {
+    train = buildTrain();
+    train.scale.set(0.075, 0.10, 0.09);
+    train.visible = false;
+    scene.add(train);
+}
+
+function buildRailsFoundation() {
+    const railsFoundationGeometry = buildRailsFoundationGeometry();
+
+    textures.durmientes.object.wrapS = THREE.RepeatWrapping;
+    textures.durmientes.object.wrapT = THREE.RepeatWrapping;
+    textures.durmientes.object.repeat.set(1, 150);
+    textures.durmientes.object.anisotropy = 16;
+
+    // load into `map` the example texture
+    const map = new THREE.TextureLoader().load(
+        'https://threejs.org/examples/textures/uv_grid_opengl.jpg');
+    map.wrapS = map.wrapT = THREE.RepeatWrapping;
+    map.repeat.set(1, 80);
+    map.anisotropy = 16;
+    // map.rotation = Math.PI/2;
+
+    const railsFoundationMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        map: textures.durmientes.object
+        // map: map
+    });
+
+    const railsFoundation = new THREE.Mesh(railsFoundationGeometry, railsFoundationMaterial);
+    railsFoundation.position.set(-1, 1.25, -1);
+    railsFoundation.scale.set(1.00, 1.50, 1.00);
+    scene.add(railsFoundation);
+}
+
+function buildRails() {
+    const railsGeometry = buildRailsGeometry();
+    const railsMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        color: 0xFFFFFF
+    });
+
+    const rails = new THREE.Mesh(railsGeometry, railsMaterial);
+    rails.position.set(-1, 1.25, -1);
+    rails.scale.set(1.00, 1.50, 1.00);
+    scene.add(rails);
+}
+
+function buildTerrain() {
+    // console.log('Building scene');
+
+    const width = 100;
+    const height = 100;
+
+    terrainGeometry = elevationGeometry(
+        width, height,
+        amplitude,
+        widthSegments, heightSegments,
+        textures.elevationMap.object);
+
+    console.log('Applying textures');
+    terrainMaterial = new THREE.RawShaderMaterial({
+        uniforms: {
+            dirtSampler: { type: 't', value: textures.tierra.object },
+            rockSampler: { type: 't', value: textures.roca.object },
+            grassSampler: { type: 't', value: textures.pasto.object },
+            scale: { type: 'f', value: 3.0 },
+            terrainAmplitude: { type: 'f', value: amplitude },
+            terrainAmplitudeBottom: { type: 'f', value: amplitudeBottom },
+            worldNormalMatrix: { type: 'm4', value: null },
+            dirtStepWidth: { type: 'f', value: 0.20 },
+            rockStepWidth: { type: 'f', value: 0.15 },
+        },
+        vertexShader: vertexShader,
+        fragmentShader: fragmentShader,
+        side: THREE.DoubleSide,
+    });
+    terrainMaterial.needsUpdate = true;
+
+    terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
+    terrain.position.set(0, amplitudeBottom, 0);
+    scene.add(terrain);
+
+    console.log('Generating water');
+    const waterGeometry = new THREE.PlaneGeometry(width/2, height);
+    const waterMaterial = new THREE.MeshPhongMaterial( {color: 0x12ABFF, side: THREE.DoubleSide} );
+    const water = new THREE.Mesh( waterGeometry, waterMaterial );
+    water.rotateX(Math.PI/2);
+    water.position.set(0, 0, 0);
+    scene.add(water);
+}
+
+function buildTunnel() {
+    const tunnelGeometry = generateTunnelGeometry();
+
+    textures.madera.object.wrapS = THREE.RepeatWrapping;
+    textures.madera.object.wrapT = THREE.RepeatWrapping;
+    textures.madera.object.repeat.set(0.10, 0.10);
+    textures.madera.object.anisotropy = 16;
+
+    const tunnelMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        map: textures.madera.object
+    });
+
+    const tunnel = new THREE.Mesh(tunnelGeometry, tunnelMaterial) ;
+    tunnel.scale.set(0.5, 0.5, 0.5);
+    scene.add(tunnel);
+}
+
+function buildTrees(count = 50) {
+    const [treeLogs, treeLeaves] = createInstancedTrees(count);
+    scene.add(treeLogs);
+    scene.add(treeLeaves);
+}
+
+function createMenu() {
+    const gui = new dat.GUI({ width: 250 });
+    gui.add(settings, 'animationEnable', true).name('Animations enabled');
+    gui.add(settings, 'showTrain', false).name('Show train').onChange(
+        function () {
+            train.visible = !train.visible;
+        });
+    // console.log(settings.animationEnable);
+}
+
+function buildScene() {
+    console.log('Building scene');
+    // buildTunnel();
+    buildTrees(200);
+    buildTerrain();
+    buildRailsFoundation();
+    buildRails();
+    buildLoco();
+    buildBridge();
+}
+
+function mainLoop() {
+    requestAnimationFrame(mainLoop);
+    renderer.render(scene, camera);
+
+    train.position.set(-10, 2.25, 0);
+
+    const dt = 0.001;
+    if(settings.animationEnable) {
+        time = (time < 1.0-dt) ? (time + dt) : 0.00;
+    }
+
+    if(train.visible) {
+        updateTrainCrankPosition(time*100);
+        const trainPos = getRailsPathPosAt(time);
+        const railsData = getRailsPathPosAt(time);
+        // [railsPath.getPointAt(t), railsPath.getTangentAt(t)]
+
+        let x = railsData[0].x;
+        let z = railsData[0].z;
+
+        // translationMatrix.makeTranslation(trainPos);
+        // rotMatrix.identity();
+
+        // translationMatrix.makeTranslation(trainPos);
+        // train.position.set(0, 0, 0);
+        // train.position.set(time*10, 1.9, 0);
+        train.position.set(-1+x, 2.25, -1+z);
+        // railsFoundation.position.set(-1, 1.25, -1);
+        train.lookAt(railsData[1].x*1000, 1.9, railsData[1].z*1000);
+        // train.lookAt(0, 1.9, 0);
+    }
+    renderer.render(scene, camera);
+}
+
+function main() {
+    setupThreeJs();
+    time = 0.00;
+    buildScene();
+    createMenu();
+    mainLoop();
+}
+
+loadTextures(main);
diff --git a/tp/src/standalone/bridge.js b/tp/src/standalone/bridge.js
@@ -0,0 +1,342 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
+
+let scene, camera, renderer, container;
+
+const textures = {
+    tierra:     { url: '/assets/tierraSeca.jpg', object: null },
+    ladrillos:  { url: '/assets/pared-de-ladrillos.jpg', object: null },
+};
+
+function onResize() {
+    camera.aspect = container.offsetWidth / container.offsetHeight;
+    camera.updateProjectionMatrix();
+    renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+    scene = new THREE.Scene();
+    container = document.getElementById('mainContainer');
+
+    renderer = new THREE.WebGLRenderer();
+    renderer.setClearColor(0x606060);
+    // renderer.setClearColor(0xFFFFFF);
+    container.appendChild(renderer.domElement);
+
+    camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000);
+    camera.position.set(25, 25, 25);
+    camera.lookAt(10, 10, 10);
+
+    const controls = new OrbitControls(camera, renderer.domElement);
+
+    const ambientLight = new THREE.AmbientLight(0xaaaaaa);
+    scene.add(ambientLight);
+
+    const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+    scene.add(hemisphereLight);
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+    directionalLight.position.set(100, 100, 100);
+    scene.add(directionalLight);
+
+    const gridHelper = new THREE.GridHelper(50, 20);
+    scene.add(gridHelper);
+
+    const axesHelper = new THREE.AxesHelper(5);
+    scene.add(axesHelper);
+
+    window.addEventListener('resize', onResize);
+    onResize();
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+const arcWidth     = 5;
+const arcCount     = 4;
+const arcRadius    = arcWidth/2;
+const columnHeight = 5;
+const columnWidth  = 1.50;
+const topPadding   = 0.50;
+const startPadding = 10;
+const endPadding   = startPadding;
+const bridgeWallThickness = 2.5;
+const bridgeLen           = arcCount*(columnWidth+arcWidth)+columnWidth+startPadding+endPadding;
+const bridgeHeight        = columnHeight+arcRadius+topPadding;
+
+function generateBridgeWall() {
+    const path = new THREE.Path();
+
+    // generate the arcs
+    for(let i = 1; i <= arcCount; ++i) {
+        path.lineTo(startPadding+i*columnWidth+((i-1)*arcWidth), 0);
+        path.moveTo(startPadding+i*columnWidth+((i-1)*arcWidth), 0);
+        path.lineTo(startPadding+i*columnWidth+((i-1)*arcWidth), columnHeight);
+        path.arc(arcRadius, 0, arcRadius, Math.PI, 0, true)
+        path.moveTo(startPadding+i*(columnWidth+arcWidth), 0);
+        path.lineTo(startPadding+i*(columnWidth+arcWidth), 0);
+    }
+
+    // no we close the curve
+    path.lineTo(bridgeLen, 0);
+    path.lineTo(bridgeLen, bridgeHeight);
+
+    path.lineTo(0, bridgeHeight);
+    path.lineTo(0, 0);
+
+    /*
+    // muestra la curva utilizada para la extrusión
+    const geometry = new THREE.BufferGeometry().setFromPoints(points);
+    const lineMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
+    const curveObject = new THREE.Line(geometry, lineMaterial);
+    scene.add(curveObject);
+    */
+
+    const points = path.getPoints();
+    const shape = new THREE.Shape(points);
+
+    const extrudeSettings = {
+        curveSegments: 24,
+        steps: 50,
+        depth: bridgeWallThickness,
+        bevelEnabled: false
+    };
+
+    const bridgeWallGeometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
+    bridgeWallGeometry.translate(-bridgeLen/2, 0, -bridgeWallThickness/2);
+    return bridgeWallGeometry;
+}
+
+const squareTubeRadius = 0.15;
+function generateBridgeCage(squaresCount = 3) {
+    const squaresSideLen = 10;
+    const bridgeCageLen  = squaresCount * squaresSideLen;
+
+    let geometries = []
+
+    let cylinderBase, cylinderCorner, cylinderCrossbar;
+    for(let square = 0; square < squaresCount; ++square) {
+        // 0 -> 00
+        // 1 -> 01
+        // 2 -> 10
+        // 3 -> 11
+        for(let i = 0; i < 4; ++i) {
+            cylinderBase = new THREE.CylinderGeometry(
+                squareTubeRadius, squareTubeRadius, squaresSideLen);
+
+            cylinderCorner = cylinderBase.clone();
+
+            const squareHypotenuse = Math.sqrt(2*squaresSideLen*squaresSideLen);
+            cylinderCrossbar = new THREE.CylinderGeometry(
+                squareTubeRadius, squareTubeRadius, squareHypotenuse);
+
+            if((i % 2) == 0) {
+                cylinderBase.rotateZ(Math.PI/2);
+                cylinderBase.translate(
+                    0,
+                    square*(squaresSideLen),
+                    ((-1)**(i>>1))*squaresSideLen/2);
+
+                cylinderCrossbar.rotateZ((-1)**((i>>1))*Math.PI/4);
+                cylinderCrossbar.translate(
+                    0,
+                    square*(squaresSideLen)+(squaresSideLen/2),
+                    ((-1)**(i>>1))*squaresSideLen/2);
+
+                cylinderCorner.translate(
+                    ((-1)**(i>>1))*squaresSideLen/2,
+                    square*(squaresSideLen)+(squaresSideLen/2),
+                    ((-1)**(i&1))*squaresSideLen/2);
+            } else {
+                cylinderBase.rotateX(Math.PI/2);
+                cylinderBase.translate(
+                    ((-1)**(i>>1))*squaresSideLen/2,
+                    square*(squaresSideLen),
+                    0);
+
+                cylinderCrossbar.rotateX((-1)**((i>>1))*Math.PI/4);
+                cylinderCrossbar.translate(
+                    ((-1)**(i>>1))*squaresSideLen/2,
+                    square*(squaresSideLen)+(squaresSideLen/2),
+                    0);
+
+                cylinderCorner.translate(
+                    ((-1)**(i>>1))*squaresSideLen/2,
+                    square*(squaresSideLen)+(squaresSideLen/2),
+                    ((-1)**(i&1))*squaresSideLen/2);
+            }
+            geometries.push(cylinderBase);
+            geometries.push(cylinderCrossbar);
+            geometries.push(cylinderCorner);
+        }
+
+        // agregamos un cuadrado mas para 'cerrar' la 'jaula'
+        if((square + 1) == squaresCount) {
+            for(let i = 0; i < 4; ++i) {
+                cylinderBase = new THREE.CylinderGeometry(
+                    squareTubeRadius, squareTubeRadius, squaresSideLen);
+
+                if((i % 2) == 0) {
+                    cylinderBase.rotateZ(Math.PI/2);
+                    cylinderBase.translate(
+                        0,
+                        (square+1)*(squaresSideLen),
+                        ((-1)**(i>>1))*squaresSideLen/2);
+                } else {
+                    cylinderBase.rotateX(Math.PI/2);
+                    cylinderBase.translate(
+                        ((-1)**(i>>1))*squaresSideLen/2,
+                        (square+1)*(squaresSideLen), 0);
+                }
+                geometries.push(cylinderBase);
+            }
+        }
+    }
+
+    const bridgeCage = mergeGeometries(geometries);
+    bridgeCage.rotateZ(Math.PI/2);
+    bridgeCage.translate(bridgeCageLen/2, squaresSideLen/2, 0);
+    return bridgeCage;
+}
+
+function generateBridge() {
+    const bridgeWidth   = 10;
+    const roadwayHeight = 2;
+
+    const leftWallGeometry = generateBridgeWall();
+    leftWallGeometry.translate(0, 0, -bridgeWidth/2);
+
+    const rightWallGeometry = generateBridgeWall();
+    rightWallGeometry.translate(0, 0, bridgeWidth/2)
+
+    const bridgeColumnsGeometry = mergeGeometries([leftWallGeometry, rightWallGeometry]);
+    const bridgeRoadwayGeometry = new THREE.BoxGeometry(
+        bridgeLen, roadwayHeight, bridgeWidth+bridgeWallThickness,
+    );
+
+    bridgeRoadwayGeometry.translate(0, bridgeHeight+roadwayHeight/2, 0);
+
+    textures.ladrillos.object.wrapS = THREE.RepeatWrapping;
+    textures.ladrillos.object.wrapT = THREE.RepeatWrapping;
+    textures.ladrillos.object.repeat.set(0.75*0.15, 0.75*0.35);
+    textures.ladrillos.object.anisotropy = 16;
+
+    const bridgeMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        map: textures.ladrillos.object
+    });
+
+    /*
+    textures.ladrillos2.object.wrapS = THREE.RepeatWrapping;
+    textures.ladrillos2.object.wrapT = THREE.RepeatWrapping;
+    textures.ladrillos2.object.repeat.set(0.75*5, 0.75*0.75);
+    textures.ladrillos2.object.anisotropy = 16;
+
+    const roadwayMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        map: textures.ladrillos2.object
+        // color: 0xFF0000
+    });
+
+    const bridgeRoadway = new THREE.Mesh(bridgeRoadwayGeometry, roadwayMaterial);
+    scene.add(bridgeRoadway);
+    */
+
+    const bridgeColumns = new THREE.Mesh(bridgeColumnsGeometry, bridgeMaterial);
+    scene.add(bridgeColumns);
+
+    // para reutilizar la textura de ladrillos usada en los arcos se escalan las
+    // coordenadas uv de la geometria de la parte superior
+    let uvs = bridgeRoadwayGeometry.attributes.uv.array;
+    for (let i = 0, len = uvs.length; i < len; i++) {
+        uvs[i] = (i % 2) ? uvs[i]*2.50 : uvs[i]*30.0;
+    }
+
+    const bridgeRoadway = new THREE.Mesh(bridgeRoadwayGeometry, bridgeMaterial);
+    scene.add(bridgeRoadway);
+
+    const cageGeometry = generateBridgeCage()
+    cageGeometry.translate(0, bridgeHeight+roadwayHeight-squareTubeRadius*2, 0);
+
+    const cageMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        color: 0xFFFFFF
+    });
+
+    const bridgeCage = new THREE.Mesh(cageGeometry, cageMaterial);
+    scene.add(bridgeCage);
+
+    const roadwayFloorGeometry = new THREE.PlaneGeometry(
+        bridgeWidth+bridgeWallThickness,
+        bridgeLen);
+
+    roadwayFloorGeometry.rotateZ(Math.PI/2)
+    roadwayFloorGeometry.rotateX(Math.PI/2)
+    roadwayFloorGeometry.translate(0, bridgeHeight+roadwayHeight, 0)
+
+    textures.tierra.object.wrapS = THREE.MirroredRepeatWrapping;
+    textures.tierra.object.wrapT = THREE.MirroredRepeatWrapping;
+    textures.tierra.object.repeat.set(1, 5);
+    textures.tierra.object.anisotropy = 16;
+
+    const roadwayFloorMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        map: textures.tierra.object
+    });
+
+    const roadwayFloor = new THREE.Mesh(roadwayFloorGeometry, roadwayFloorMaterial);
+    scene.add(roadwayFloor)
+}
+
+function mainLoop() {
+    requestAnimationFrame(mainLoop);
+    renderer.render(scene, camera);
+}
+
+function main() {
+    generateBridge();
+    mainLoop();
+}
+
+setupThreeJs();
+loadTextures(main);
diff --git a/tp/src/standalone/rails.js b/tp/src/standalone/rails.js
@@ -0,0 +1,274 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+
+import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js';
+import { ParametricGeometries } from 'three/examples/jsm/geometries/ParametricGeometries.js';
+import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
+
+let scene, camera, renderer, container, terrainMaterial, instancedTrees;
+let spherePath;
+let railsPath;
+let railsFoundationShape;
+
+const textures = {
+    tierra:     { url: '/assets/tierra.jpg', object: null },
+    roca:       { url: '/assets/roca.jpg', object: null },
+    pasto:      { url: '/assets/pasto.jpg', object: null },
+    durmientes: { url: '/assets/durmientes.jpg', object: null },
+};
+
+function onResize() {
+    camera.aspect = container.offsetWidth / container.offsetHeight;
+    camera.updateProjectionMatrix();
+    renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+    scene = new THREE.Scene();
+    container = document.getElementById('mainContainer');
+
+    renderer = new THREE.WebGLRenderer();
+    renderer.setClearColor(0x606060);
+    container.appendChild(renderer.domElement);
+
+    camera = new THREE.PerspectiveCamera(
+        35, window.innerWidth / window.innerHeight, 0.1, 1000);
+
+    camera.position.set(-10, 15, -10);
+    camera.lookAt(0, 0, 0);
+
+    const controls = new OrbitControls(camera, renderer.domElement);
+
+    const ambientLight = new THREE.AmbientLight(0xffffff);
+    scene.add(ambientLight);
+
+    const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+    scene.add(hemisphereLight);
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+    directionalLight.position.set(100, 100, 100);
+    scene.add(directionalLight);
+
+    const gridHelper = new THREE.GridHelper(50, 20);
+    scene.add(gridHelper);
+
+    const axesHelper = new THREE.AxesHelper(5);
+    scene.add(axesHelper);
+
+    window.addEventListener('resize', onResize);
+    onResize();
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+function parametricRailsFoundationFunction(u, v, target) {
+    const rotMatrix = new THREE.Matrix4();
+    const translationMatrix = new THREE.Matrix4();
+    const levelMatrix = new THREE.Matrix4();
+
+    let railsPathPos = railsPath.getPointAt(v);
+    let railsFoundationShapePos = railsFoundationShape.getPointAt(u);
+    // TODO: make `railsFoundationShape` smaller and remove this multiplication
+    railsFoundationShapePos.multiplyScalar(0.5);
+
+    let tangente = new THREE.Vector3();
+    let binormal = new THREE.Vector3();
+    let normal = new THREE.Vector3();
+
+    tangente = railsPath.getTangent(v);
+
+    tangente.normalize();
+    binormal = new THREE.Vector3(0, 1, 0);
+    normal.crossVectors(tangente, binormal);
+
+    translationMatrix.makeTranslation(railsPathPos);
+
+    rotMatrix.identity();
+    levelMatrix.identity();
+
+    levelMatrix.makeTranslation(railsPathPos);
+    rotMatrix.makeBasis(normal, tangente, binormal);
+    levelMatrix.multiply(rotMatrix);
+    railsFoundationShapePos.applyMatrix4(levelMatrix);
+    
+    const x = railsFoundationShapePos.x;
+    const y = railsFoundationShapePos.y;
+    const z = railsFoundationShapePos.z;
+    target.set(x, y, z);
+}
+
+export function buildRailsFoundation() {
+    railsFoundationShape = new THREE.CatmullRomCurve3([
+        new THREE.Vector3( -2.00, 0.00, 0.00),
+        new THREE.Vector3( -1.00, 0.00, 0.50),
+        new THREE.Vector3(  0.00, 0.00, 0.55),
+        new THREE.Vector3(  1.00, 0.00, 0.50),
+        new THREE.Vector3(  2.00, 0.00, 0.00),
+    ], false);
+
+    /*
+    // show rails foundation shape
+    const points = railsFoundationShape.getPoints(50);
+    const geometry = new THREE.BufferGeometry().setFromPoints(points);
+    const lineMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
+    const curveObject = new THREE.Line(geometry, lineMaterial);
+    scene.add(curveObject);
+    */
+    const pGeometry = new ParametricGeometry(
+        parametricRailsFoundationFunction, 100, 100);
+    
+    textures.durmientes.object.wrapS = THREE.RepeatWrapping;
+    textures.durmientes.object.wrapT = THREE.RepeatWrapping;
+    textures.durmientes.object.repeat.set(1, 60);
+    textures.durmientes.object.anisotropy = 16;
+
+    /*
+    // load into `map` the example texture
+    const map = new THREE.TextureLoader().load('https://threejs.org/examples/textures/uv_grid_opengl.jpg');
+    map.wrapS = map.wrapT = THREE.RepeatWrapping;
+    map.repeat.set(1, 30);
+    map.anisotropy = 16;
+    // map.rotation = Math.PI/2;
+    */
+
+    const pMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        map: textures.durmientes.object
+    });
+    const pMesh = new THREE.Mesh(pGeometry, pMaterial);
+    scene.add(pMesh);
+}
+
+// `position` es de tipo `THREE.Vector3` y representa la translacion de la
+// forma del rail con respecto al origen del sist. de coordenadas de modelado
+function getParametricRailsFunction(radius, position) {
+    return function parametricRails(u, v, target) {
+        const rotMatrix = new THREE.Matrix4();
+        const translationMatrix = new THREE.Matrix4();
+        const levelMatrix = new THREE.Matrix4();
+
+        let railsShape = new THREE.Vector3();
+
+        let railsPathPos = railsPath.getPointAt(v);
+        let railsShapePos = new THREE.Vector3(
+            Math.cos(u*6.28) + position.x,
+            position.y,
+            Math.sin(u*6.28) + position.z);
+
+        railsShapePos.multiplyScalar(0.1*railsRadius);
+
+        let tangente = new THREE.Vector3();
+        let binormal = new THREE.Vector3();
+        let normal = new THREE.Vector3();
+
+        // https://threejs.org/docs/index.html?q=curve#api/en/extras/core/Curve.getTangent
+        tangente = railsPath.getTangentAt(v);
+        binormal = new THREE.Vector3(0, 1, 0);
+        normal.crossVectors(tangente, binormal);
+
+        translationMatrix.makeTranslation(railsPathPos);
+
+        rotMatrix.identity();
+        levelMatrix.identity();
+
+        levelMatrix.makeTranslation(railsPathPos);
+        rotMatrix.makeBasis(normal, tangente, binormal);
+        levelMatrix.multiply(rotMatrix);
+        railsShapePos.applyMatrix4(levelMatrix);
+        
+        const x = railsShapePos.x;
+        const y = railsShapePos.y;
+        const z = railsShapePos.z;
+        target.set(x, y, z);
+    }
+}
+
+const railsRadius = 0.35;
+function buildRails() {
+    let railsGeometries = [];
+
+    const leftRailGeometryFunction  = getParametricRailsFunction(railsRadius,
+        new THREE.Vector3( 6, 0, railsRadius+8));
+
+    const rightRailGeometryFunction = getParametricRailsFunction(railsRadius,
+        new THREE.Vector3(-6, 0, railsRadius+8));
+
+    const leftRailGeometry  = new ParametricGeometry(leftRailGeometryFunction, 100, 500);
+    const rightRailGeometry = new ParametricGeometry(rightRailGeometryFunction, 100, 500);
+
+    railsGeometries.push(leftRailGeometry);
+    railsGeometries.push(rightRailGeometry);
+
+    const railsMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        color: 0xFFFFFF
+    });
+
+    const railsGeometry = mergeGeometries(railsGeometries);
+    const rails = new THREE.Mesh(railsGeometry, railsMaterial);
+    scene.add(rails);
+}
+
+function mainLoop() {
+    requestAnimationFrame(mainLoop);
+    renderer.render(scene, camera);
+}
+
+function main() {
+    railsPath = new THREE.CatmullRomCurve3([
+        new THREE.Vector3(-10, 0,  10),
+        new THREE.Vector3( 10, 0,  10),
+        new THREE.Vector3( 10, 0, -10),
+        new THREE.Vector3(-10, 0, -10),
+    ], true);
+
+    /*
+    // muestra la curva utilizada para el camino de `rails`
+    const railsPathPoints = railsPath.getPoints(50);
+    const railsPathGeometry = new THREE.BufferGeometry().setFromPoints(railsPathPoints);
+    const railsPathMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
+    const railsPathMesh = new THREE.Line(railsPathGeometry, railsPathMaterial);
+    scene.add(railsPathMesh);
+    */
+
+    buildRailsFoundation();
+    buildRails();
+    mainLoop();
+}
+
+setupThreeJs();
+loadTextures(main);
diff --git a/tp/src/standalone/terrain.js b/tp/src/standalone/terrain.js
@@ -0,0 +1,371 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { vertexShader, fragmentShader } from '/assets/shaders.js';
+
+let scene, camera, renderer, container, terrainMaterial, terrainGeometry, terrain;
+
+const widthSegments = 100;
+const heightSegments = 100;
+const amplitude = 8;
+const amplitudeBottom = -1.00;
+
+const textures = {
+    tierra: { url: '/assets/tierra.jpg', object: null },
+    roca: { url: '/assets/roca.jpg', object: null },
+    pasto: { url: '/assets/pasto.jpg', object: null },
+    elevationMap: { url: '/assets/elevation_map2.png', object: null },
+};
+
+function onResize() {
+    camera.aspect = container.offsetWidth / container.offsetHeight;
+    camera.updateProjectionMatrix();
+    renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+    scene = new THREE.Scene();
+    container = document.getElementById('mainContainer');
+
+    renderer = new THREE.WebGLRenderer();
+    renderer.setClearColor(0x606060);
+    container.appendChild(renderer.domElement);
+
+    camera = new THREE.PerspectiveCamera(
+        35, window.innerWidth/window.innerHeight, 0.1, 1000);
+    camera.position.set(100, 120, -100);
+    camera.lookAt(0, 0, 0);
+
+    const controls = new OrbitControls(camera, renderer.domElement);
+
+    const ambientLight = new THREE.AmbientLight(0xffffff);
+    scene.add(ambientLight);
+
+    const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+    //scene.add(hemisphereLight);
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+    directionalLight.position.set(100, 100, 100);
+    scene.add(directionalLight);
+
+    const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
+    // scene.add(directionalLightHelper);
+
+     const gridHelper = new THREE.GridHelper(150, 150);
+     scene.add(gridHelper);
+
+    const axesHelper = new THREE.AxesHelper( 5 );
+    scene.add( axesHelper );
+
+    window.addEventListener('resize', onResize);
+    onResize();
+}
+
+// obtiene una posicion aleatoria en el terreno, para obtener la altura del
+// terreno utiliza el mapa de elevacion
+function getRandomPositionInTerrain() {
+    let canvas = document.createElement('canvas');
+    let ctx = canvas.getContext('2d');
+    let img = textures.elevationMap.object.image;
+
+    canvas.width  = widthSegments;
+    canvas.height = heightSegments;
+
+    ctx.drawImage(img, 0, 0, widthSegments, heightSegments);
+    let imageData = ctx.getImageData(0, 0, widthSegments, heightSegments);
+    let data = imageData.data;
+    const quadsPerRow = widthSegments - 1;
+
+    const x = Math.random();
+    const z = Math.random();
+
+    const elevationMapData = Math.floor((x + z) * widthSegments);
+    const indexX = Math.floor(x * widthSegments);
+    const indexZ = Math.floor(z * heightSegments);
+    const y = data[(indexX + indexZ * widthSegments) * 4] / 255;
+
+    const position = new THREE.Vector3((
+        x - 0.5) * widthSegments,
+        y * amplitude,
+        (z - 0.5) * heightSegments);
+
+    return position;
+}
+
+function createInstancedTrees(count) {
+    console.log('Generating `' + count + '` instances of tree');
+
+    let logHeight = 4.0;
+    const treeLogGeometry   = new THREE.CylinderGeometry(
+        0.30, 0.30, logHeight, 40, 40);
+    treeLogGeometry.translate(0, logHeight/2.0, 0);
+    const instancedTreeLogGeometry = new THREE.InstancedBufferGeometry();
+    instancedTreeLogGeometry.copy(treeLogGeometry);
+    const treeLogMaterial   = new THREE.MeshPhongMaterial({color: 0x7c3f00});
+    const instancedTreeLogs = new THREE.InstancedMesh(
+        instancedTreeLogGeometry,
+        treeLogMaterial,
+        count);
+
+    const treeLeavesGeometry = new THREE.SphereGeometry(1.75,40,40);
+    const instancedTreeLeavesGeometry = new THREE.InstancedBufferGeometry();
+    instancedTreeLeavesGeometry.copy(treeLeavesGeometry);
+    const treeLeavesMaterial  = new THREE.MeshPhongMaterial({color: 0x365829});
+    const instancedTreeLeaves = new THREE.InstancedMesh(
+        instancedTreeLeavesGeometry,
+        treeLeavesMaterial,
+        count);
+
+    const rotMatrix         = new THREE.Matrix4();
+    const translationMatrix = new THREE.Matrix4();
+    const treeLogMatrix     = new THREE.Matrix4();
+    const treeLeavesMatrix  = new THREE.Matrix4();
+
+    for (let i = 0; i < count; i++) {
+        let position = getRandomPositionInTerrain();
+        let j = 0;
+        while((position.y > 4.0) || (position.y < 2.5)) {
+            position = getRandomPositionInTerrain();
+            // console.log(position);
+            if(j++ == 100) {
+                break;
+            }
+        }
+
+        position.y += amplitudeBottom;
+        translationMatrix.makeTranslation(position);
+        treeLogMatrix.identity();
+        treeLeavesMatrix.identity();
+
+        let scale = 0.5 + (Math.random()*(logHeight/3));
+        treeLogMatrix.makeScale(1, scale, 1);
+        treeLogMatrix.premultiply(translationMatrix);
+
+        position.y += scale * logHeight;
+        translationMatrix.makeTranslation(position);
+        treeLeavesMatrix.premultiply(translationMatrix);
+
+        instancedTreeLogs.setMatrixAt(i, treeLogMatrix);
+        instancedTreeLeaves.setMatrixAt(i, treeLeavesMatrix);
+    }
+
+    return [instancedTreeLogs, instancedTreeLeaves];
+}
+
+// La funcion devuelve una geometria de Three.js
+// width: Ancho del plano
+// height: Alto del plano
+// amplitude: Amplitud de la elevacion
+// widthSegments: Numero de segmentos en el ancho
+// heightSegments: Numero de segmentos en el alto
+// texture: Textura que se usara para la elevacion
+function elevationGeometry(width, height, amplitude, widthSegments, heightSegments, texture) {
+    console.log('Generating terrain geometry');
+    let geometry = new THREE.BufferGeometry();
+
+    const positions = [];
+    const indices = [];
+    const normals = [];
+    const uvs = [];
+
+    // Creamos un canvas para poder leer los valores de los píxeles de la textura
+    let canvas = document.createElement('canvas');
+    let ctx = canvas.getContext('2d');
+    let img = texture.image;
+
+    // Ajustamos el tamaño del canvas segun la cantidad de segmentos horizontales y verticales
+    canvas.width = widthSegments;
+    canvas.height = heightSegments;
+
+    // Dibujamos la textura en el canvas en la escala definida por widthSegments y heightSegments
+    ctx.drawImage(img, 0, 0, widthSegments, heightSegments);
+
+    // Obtenemos los valores de los píxeles de la textura
+    let imageData = ctx.getImageData(0, 0, widthSegments, heightSegments);
+    let data = imageData.data; // Este es un array con los valores de los píxeles
+
+    const quadsPerRow = widthSegments - 1;
+
+    // Recorremos los segmentos horizontales y verticales
+    for (let i = 0; i < widthSegments - 1; i++) {
+        for (let j = 0; j < heightSegments - 1; j++) {
+            // Obtenemos los valores de los píxeles de los puntos adyacentes
+            let xPrev = undefined;
+            let xNext = undefined;
+            let yPrev = undefined;
+            let yNext = undefined;
+
+            // Obtenemos el valor del pixel en la posicion i, j
+            // console.log('getting elevation map value at: (' + i + ',' + j + ')');
+            let z0 = data[(i + j * widthSegments) * 4] / 255;
+
+            // Obtenemos los valores de los píxeles adyacentes
+            xPrev = i > 0 ? data[(i - 1 + j * widthSegments) * 4] / 255 : undefined;
+            xNext = i < widthSegments - 1 ? (xNext = data[(i + 1 + j * widthSegments) * 4] / 255) : undefined;
+
+            yPrev = j > 0 ? data[(i + (j - 1) * widthSegments) * 4] / 255 : undefined;
+            yNext = j < heightSegments - 1 ? data[(i + (j + 1) * widthSegments) * 4] / 255 : undefined;
+
+            // calculamos la diferencia entre los valores de los píxeles adyacentes
+            // en el eje `x` y en el eje `y` de la imagen (en el espacio de la textura
+            // Ojo no confundir con el espacio 3D del modelo 3D donde Y es la altura)
+            let deltaX;
+            if (xPrev == undefined) {
+                deltaX = xNext - z0;
+            } else if (yNext == undefined) {
+                deltaX = xPrev - z0;
+            } else {
+                deltaX = (xNext - xPrev) / 2;
+            }
+
+            let deltaY;
+            if (yPrev == undefined) {
+                deltaY = yNext - z0;
+            } else if (yNext == undefined) {
+                deltaY = yPrev - z0;
+            } else {
+                deltaY = (yNext - yPrev) / 2;
+            }
+
+            // Calculamos la altura del punto en el espacio 3D
+            const z = amplitude * z0;
+
+            // Añadimos los valores de los puntos al array de posiciones
+            positions.push((width * i) / widthSegments - width / 2);
+            positions.push(z);
+            positions.push((height * j) / heightSegments - height / 2);
+
+            // Calculamos los vectores tangentes a la superficie en el ejex y en el eje y
+            let tanX = new THREE.Vector3(width / widthSegments, deltaX * amplitude, 0).normalize();
+            let tanY = new THREE.Vector3(0, deltaY * amplitude, height / heightSegments).normalize();
+
+            // Calculamos el vector normal a la superficie
+            let n = new THREE.Vector3();
+            n.crossVectors(tanY, tanX);
+
+            // Añadimos los valores de los vectores normales al array de normales
+            normals.push(n.x);
+            normals.push(n.y);
+            normals.push(n.z);
+
+            uvs.push(i / (widthSegments - 1));
+            uvs.push(j / (heightSegments - 1));
+
+            if (i == widthSegments - 2 || j == heightSegments - 2) continue;
+
+            // Ensamblamos los triangulos
+            indices.push(i + j * quadsPerRow);
+            indices.push(i + 1 + j * quadsPerRow);
+            indices.push(i + 1 + (j + 1) * quadsPerRow);
+
+            indices.push(i + j * quadsPerRow);
+            indices.push(i + 1 + (j + 1) * quadsPerRow);
+            indices.push(i + (j + 1) * quadsPerRow);
+        }
+    }
+
+    geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
+    geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
+    geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
+    geometry.setIndex(indices);
+
+    return geometry;
+}
+
+function buildScene() {
+    console.log('Building scene');
+
+    const width = 100;
+    const height = 100;
+
+    terrainGeometry = elevationGeometry(
+        width, height,
+        amplitude,
+        widthSegments, heightSegments,
+        textures.elevationMap.object);
+
+    console.log('Applying textures');
+    terrainMaterial = new THREE.RawShaderMaterial({
+        uniforms: {
+            dirtSampler: { type: 't', value: textures.tierra.object },
+            rockSampler: { type: 't', value: textures.roca.object },
+            grassSampler: { type: 't', value: textures.pasto.object },
+            scale: { type: 'f', value: 3.0 },
+            terrainAmplitude: { type: 'f', value: amplitude },
+            terrainAmplitudeBottom: { type: 'f', value: amplitudeBottom },
+            worldNormalMatrix: { type: 'm4', value: null },
+            dirtStepWidth: { type: 'f', value: 0.20 },
+            rockStepWidth: { type: 'f', value: 0.15 },
+        },
+        vertexShader: vertexShader,
+        fragmentShader: fragmentShader,
+        side: THREE.DoubleSide,
+    });
+    terrainMaterial.needsUpdate = true;
+
+    terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
+    terrain.position.set(0, amplitudeBottom, 0);
+    scene.add(terrain);
+
+    console.log('Generating water');
+    const waterGeometry = new THREE.PlaneGeometry(width/2, height);
+    const waterMaterial = new THREE.MeshPhongMaterial( {color: 0x12ABFF, side: THREE.DoubleSide} );
+    const water = new THREE.Mesh( waterGeometry, waterMaterial );
+    water.rotateX(Math.PI/2);
+    water.position.set(0, 0.75, 0);
+    scene.add(water);
+
+    const [treeLogs, treeLeaves] = createInstancedTrees(100);
+    scene.add(treeLogs);
+    scene.add(treeLeaves);
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+function createMenu() {
+    const gui = new dat.GUI({ width: 400 });
+    gui.add(terrainMaterial.uniforms.scale, 'value', 1.00, 5.00).name('Terrain texture scale');
+    gui.add(terrainMaterial.uniforms.dirtStepWidth, 'value', 0.0, 1.0).name('dirt step width');
+    gui.add(terrainMaterial.uniforms.rockStepWidth, 'value', 0.10, 0.50).name('rock step width');
+}
+
+function mainLoop() {
+    requestAnimationFrame(mainLoop);
+    renderer.render(scene, camera);
+}
+
+setupThreeJs();
+loadTextures(main);
+
+function main() {
+    buildScene();
+    createMenu();
+    mainLoop();
+}
diff --git a/tp/src/standalone/track-map.js b/tp/src/standalone/track-map.js
@@ -0,0 +1,423 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { vertexShader, fragmentShader } from '/assets/shaders.js';
+
+let scene, camera, renderer, container, terrainMaterial, terrainGeometry, terrain;
+let treesForbiddenMapData, treesForbiddenMap, elevationMap, elevationMapData;
+
+const widthSegments   = 100;
+const heightSegments  = 100;
+const amplitude       = 8;
+const amplitudeBottom = -1.00;
+
+const textures = {
+    tierra:           { url: '/assets/tierra.jpg', object: null },
+    roca:             { url: '/assets/roca.jpg', object: null },
+    pasto:            { url: '/assets/pasto.jpg', object: null },
+    elevationMap:     { url: '/assets/elevation_map2.png', object: null },
+    treeForbiddenMap: { url: '/assets/tree_forbidden_zone_map.png', object: null }
+};
+
+function onResize() {
+    camera.aspect = container.offsetWidth / container.offsetHeight;
+    camera.updateProjectionMatrix();
+    renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+    scene = new THREE.Scene();
+    container = document.getElementById('mainContainer');
+
+    renderer = new THREE.WebGLRenderer();
+    renderer.setClearColor(0x606060);
+    container.appendChild(renderer.domElement);
+
+    camera = new THREE.PerspectiveCamera(
+        35, window.innerWidth/window.innerHeight, 0.1, 1000);
+    camera.position.set(100, 120, -100);
+    camera.lookAt(0, 0, 0);
+
+    const controls = new OrbitControls(camera, renderer.domElement);
+
+    const ambientLight = new THREE.AmbientLight(0xffffff);
+    scene.add(ambientLight);
+
+    const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+    //scene.add(hemisphereLight);
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+    directionalLight.position.set(100, 100, 100);
+    scene.add(directionalLight);
+
+    const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
+    // scene.add(directionalLightHelper);
+
+     const gridHelper = new THREE.GridHelper(150, 150);
+     scene.add(gridHelper);
+
+    const axesHelper = new THREE.AxesHelper( 5 );
+    scene.add( axesHelper );
+
+    window.addEventListener('resize', onResize);
+    onResize();
+}
+
+const imgWidth  = 512;
+const imgHeight = 512;
+
+// (x, y) ∈ [imgHeight, imgWidth] -> son un punto de la imagen 
+function getPixelIndex(x, y) {
+    return Math.floor(x + y*imgWidth*4);
+}
+
+function getPixel(imgData, index) {
+  let i = index*4, d = imgData.data
+  return [d[i],d[i+1],d[i+2],d[i+3]] // Returns array [R,G,B,A]
+}
+
+function getPixelXY(imgData, x, y) {
+  return getPixel(imgData, y*imgData.width+x)
+}
+
+// position: Vector3
+function isForbbidenPosition(position) {
+    const x = Math.floor(position.x);
+    const y = position.y;
+    const z = Math.floor(position.z);
+
+    /*
+    if((y > 5.0) || (y < 2.65)){
+        console.log("(" + position.x + ", " + position.y + ", " + position.z + ") is not valid ");
+        return true;
+    }
+    */
+    
+    let pixelArray = getPixelXY(treesForbiddenMap, x, z);
+    const R = pixelArray[0]; // Red
+    const G = pixelArray[1]; // Green
+    const B = pixelArray[2]; // Blue
+    const A = pixelArray[3]; // Alpha
+    // const pixel = new THREE.Vector4(R, G, B, A);
+
+    if(((R <= 10) && (G >= 250) && (B <= 10))
+        || (R <= 80) && (G <= 80) && (B <= 80)
+        || (R >= 200) && (G >= 200) && (B >= 200)) {
+        // console.log("(" + position.x + ", " + position.y + ", " + position.z + ") is not valid ");
+        return true;
+    }
+
+    console.log("(" + position.x + ", " + position.y + ") is valid ");
+    return false;
+}
+
+// obtiene una posicion aleatoria en el terreno, para obtener la altura del
+// terreno utiliza el mapa de elevacion.
+// `padding` permite definir un borde del cual no se toman puntos
+function getRandomPositionInTerrain(padding = 0) {
+    const x = Math.floor(Math.random() * (widthSegments-(padding*2)));
+    const z = Math.floor(Math.random() * (heightSegments-(padding*2)));
+
+    const pixelArray = getPixelXY(elevationMap, x, z); // array [R,G,B,A]
+    const y = (pixelArray[0]/255)*amplitude;
+
+    const position = new THREE.Vector3(x+padding, y, z+padding);
+    return position;
+}
+
+function createInstancedTrees(count) {
+    console.log('Generating `' + count + '` instances of tree');
+
+    let logHeight = 4.0;
+    const treeLogGeometry   = new THREE.CylinderGeometry(
+        0.30, 0.30, logHeight, 40, 40);
+    treeLogGeometry.translate(0, logHeight/2.0, 0);
+    const instancedTreeLogGeometry = new THREE.InstancedBufferGeometry();
+    instancedTreeLogGeometry.copy(treeLogGeometry);
+    const treeLogMaterial   = new THREE.MeshPhongMaterial({color: 0x7c3f00});
+    const instancedTreeLogs = new THREE.InstancedMesh(
+        instancedTreeLogGeometry,
+        treeLogMaterial,
+        count);
+
+    const treeLeavesGeometry = new THREE.SphereGeometry(1.75,40,40);
+    const instancedTreeLeavesGeometry = new THREE.InstancedBufferGeometry();
+    instancedTreeLeavesGeometry.copy(treeLeavesGeometry);
+    const treeLeavesMaterial  = new THREE.MeshPhongMaterial({color: 0x365829});
+    const instancedTreeLeaves = new THREE.InstancedMesh(
+        instancedTreeLeavesGeometry,
+        treeLeavesMaterial,
+        count);
+
+    const rotMatrix         = new THREE.Matrix4();
+    const translationMatrix = new THREE.Matrix4();
+    const treeLogMatrix     = new THREE.Matrix4();
+    const treeLeavesMatrix  = new THREE.Matrix4();
+
+    const treesBorderPadding = 3.0;
+    for (let i = 0; i < count; i++) {
+        let position = getRandomPositionInTerrain(treesBorderPadding);
+        for(let j = 0; isForbbidenPosition(position); ++j) {
+            position = getRandomPositionInTerrain(treesBorderPadding);
+            if(j++ == 1000) { // maximo de iteraciones
+                break;
+            }
+        }
+
+        if(isForbbidenPosition(position)) {
+            continue;
+        }
+
+        const treeOffset = 0.25;
+        // 1.50 numbero magico para posicionar correctamente los arboles con
+        // respecto al terreno
+        position.x -= (widthSegments+treesBorderPadding+1.50)/2;
+        position.y += amplitudeBottom - treeOffset;
+        position.z -= (heightSegments+treesBorderPadding)/2;
+        translationMatrix.makeTranslation(position);
+        treeLogMatrix.identity();
+        treeLeavesMatrix.identity();
+
+        let scale = 0.6 + (Math.random()*(logHeight/3));
+        treeLogMatrix.makeScale(1, scale, 1);
+        treeLogMatrix.premultiply(translationMatrix);
+
+        position.y += scale*logHeight;
+        translationMatrix.makeTranslation(position);
+        treeLeavesMatrix.premultiply(translationMatrix);
+
+        instancedTreeLogs.setMatrixAt(i, treeLogMatrix);
+        instancedTreeLeaves.setMatrixAt(i, treeLeavesMatrix);
+    }
+
+    console.log('Done generating `' + count + '` instances of tree');
+    return [instancedTreeLogs, instancedTreeLeaves];
+}
+
+
+// La funcion devuelve una geometria de Three.js
+// width: Ancho del plano
+// height: Alto del plano
+// amplitude: Amplitud de la elevacion
+// widthSegments: Numero de segmentos en el ancho
+// heightSegments: Numero de segmentos en el alto
+function elevationGeometry(width, height, amplitude, widthSegments, heightSegments) {
+    console.log('Generating terrain geometry');
+    let geometry = new THREE.BufferGeometry();
+
+    const positions = [];
+    const indices = [];
+    const normals = [];
+    const uvs = [];
+
+    let imageData = elevationMap;
+    let data = elevationMapData;
+
+    const quadsPerRow = widthSegments - 1;
+
+    // Recorremos los segmentos horizontales y verticales
+    for (let x = 0; x < widthSegments - 1; x++) {
+        for (let y = 0; y < heightSegments - 1; y++) {
+            // valor del pixel en el mapa de elevacion en la posicion i, j
+
+            let pixel = getPixelXY(imageData, x, y);
+
+            // se utiliza el canal rojo [R, G, B, A];
+            let z0 = pixel[0] / 255;
+            const z = amplitude * z0;
+
+            // valores de los píxeles de los puntos adyacentes
+            let xPrev, xNext, yPrev, yNext;
+
+            xPrev = (x > 0) ? getPixelXY(imageData, x-1, y)[0]/255 : undefined;
+            xNext = (x < widthSegments-1) ? getPixelXY(imageData, x+1, y)[0]/255 : undefined;
+
+            yPrev = (y > 0) ? getPixelXY(imageData, x, y+1)[0]/255 : undefined;
+            yNext = (y < heightSegments-1) ? getPixelXY(imageData, x, y+1)[0]/255 : undefined;
+
+            // diferencia entre los valores de los píxeles adyacentes en el eje
+            // `x` y en el eje `y` de la imagen en el espacio de la textura
+            let deltaX;
+            if (xPrev == undefined) {
+                deltaX = xNext - z0;
+            } else if (yNext == undefined) {
+                deltaX = xPrev - z0;
+            } else {
+                deltaX = (xNext - xPrev) / 2;
+            }
+
+            let deltaY;
+            if (yPrev == undefined) {
+                deltaY = yNext - z0;
+            } else if (yNext == undefined) {
+                deltaY = yPrev - z0;
+            } else {
+                deltaY = (yNext - yPrev) / 2;
+            }
+
+            // Añadimos los valores de los puntos al array de posiciones
+            positions.push((width * x) / widthSegments - width / 2);
+            positions.push(z);
+            positions.push((height * y) / heightSegments - height / 2);
+
+            // vectores tangentes a la superficie en el eje `x` y en el eje `y`
+            let tanX = new THREE.Vector3(width / widthSegments, deltaX * amplitude, 0);
+            let tanY = new THREE.Vector3(0, deltaY * amplitude, height / heightSegments);
+
+            tanX.normalize();
+            tanY.normalize();
+
+            let normal = new THREE.Vector3().crossVectors(tanY, tanX);
+
+            normals.push(normal.x);
+            normals.push(normal.y);
+            normals.push(normal.z);
+
+            uvs.push(x / (widthSegments - 1));
+            uvs.push(y / (heightSegments - 1));
+
+            if ((x == widthSegments-2) || (y == heightSegments-2)) continue;
+
+            // Ensamblamos los triangulos
+            indices.push(x + y*quadsPerRow);
+            indices.push(x + 1 + y*quadsPerRow);
+            indices.push(x + 1 + (y+1)*quadsPerRow);
+
+            indices.push(x + y*quadsPerRow);
+            indices.push(x + 1 + (y+1)*quadsPerRow);
+            indices.push(x + (y+1)*quadsPerRow);
+        }
+    }
+
+    geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
+    geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
+    geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
+    geometry.setIndex(indices);
+
+    return geometry;
+}
+
+function buildScene() {
+    console.log('Building scene');
+
+    const width  = widthSegments;
+    const height = heightSegments;
+
+    terrainGeometry = elevationGeometry(
+        width, height,
+        amplitude,
+        widthSegments, heightSegments);
+
+    console.log('Applying textures');
+    terrainMaterial = new THREE.RawShaderMaterial({
+        uniforms: {
+            dirtSampler: { type: 't', value: textures.tierra.object },
+            rockSampler: { type: 't', value: textures.roca.object },
+            grassSampler: { type: 't', value: textures.pasto.object },
+            scale: { type: 'f', value: 3.0 },
+            terrainAmplitude: { type: 'f', value: amplitude },
+            terrainAmplitudeBottom: { type: 'f', value: amplitudeBottom },
+            worldNormalMatrix: { type: 'm4', value: null },
+            dirtStepWidth: { type: 'f', value: 0.20 },
+            rockStepWidth: { type: 'f', value: 0.15 },
+        },
+        vertexShader: vertexShader,
+        fragmentShader: fragmentShader,
+        side: THREE.DoubleSide,
+    });
+    terrainMaterial.needsUpdate = true;
+
+    terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
+    terrain.position.set(0, amplitudeBottom, 0);
+    scene.add(terrain);
+
+    const waterGeometry = new THREE.PlaneGeometry(width/2, height-1.55);
+    const waterMaterial = new THREE.MeshPhongMaterial( {color: 0x12ABFF, side: THREE.DoubleSide} );
+    const water = new THREE.Mesh( waterGeometry, waterMaterial );
+    water.rotateX(Math.PI/2);
+    water.position.set(0, 0.75, -1.00);
+    scene.add(water);
+
+    const [treeLogs, treeLeaves] = createInstancedTrees(250);
+    scene.add(treeLogs);
+    scene.add(treeLeaves);
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+function createMenu() {
+    const gui = new dat.GUI({ width: 400 });
+    gui.add(terrainMaterial.uniforms.scale, 'value', 1.00, 5.00).name('Terrain texture scale');
+    gui.add(terrainMaterial.uniforms.dirtStepWidth, 'value', 0.0, 1.0).name('dirt step width');
+    gui.add(terrainMaterial.uniforms.rockStepWidth, 'value', 0.10, 0.50).name('rock step width');
+}
+
+function mainLoop() {
+    requestAnimationFrame(mainLoop);
+    renderer.render(scene, camera);
+}
+
+function loadMapsData() {
+    console.log("Loading maps data");
+
+    // Creamos un canvas para poder leer los valores de los píxeles de la textura
+    let canvas = document.createElement('canvas');
+    let ctx = canvas.getContext('2d');
+
+    let treesForbiddenMapImage = textures.treeForbiddenMap.object.image;
+    let elevationMapImage = textures.elevationMap.object.image;
+
+    // ambos mapas deben tener el mismo tamaño
+    const imgWidth  = widthSegments;
+    const imgHeight = heightSegments;
+
+    canvas.width  = imgWidth;
+    canvas.height = imgHeight;
+
+    ctx.drawImage(treesForbiddenMapImage, 0, 0, imgWidth, imgHeight);
+    treesForbiddenMap = ctx.getImageData(0, 0, imgWidth, imgHeight);
+    treesForbiddenMapData = treesForbiddenMap.data;
+
+    ctx.drawImage(elevationMapImage, 0, 0, imgWidth, imgHeight);
+    elevationMap = ctx.getImageData(0, 0, imgWidth, imgHeight);
+    elevationMapData = elevationMap.data
+
+    console.log("All maps data loaded succesfully");
+}
+
+function main() {
+    loadMapsData();
+    buildScene();
+    // getTrainTrackShape();
+    mainLoop();
+}
+
+setupThreeJs();
+loadTextures(main);
diff --git a/tp/src/standalone/train.js b/tp/src/standalone/train.js
@@ -0,0 +1,338 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
+
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+
+let scene, camera, renderer, container, terrainMaterial, instancedTrees;
+
+let time = 0.0;
+
+function onResize() {
+    camera.aspect = container.offsetWidth / container.offsetHeight;
+    camera.updateProjectionMatrix();
+    renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+    scene = new THREE.Scene();
+    container = document.getElementById('mainContainer');
+
+    renderer = new THREE.WebGLRenderer();
+    renderer.setClearColor(0x606060);
+    container.appendChild(renderer.domElement);
+
+    camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000);
+    camera.position.set(-50, 60, 50);
+    camera.lookAt(0, 0, 0);
+
+    const controls = new OrbitControls(camera, renderer.domElement);
+
+    const ambientLight = new THREE.AmbientLight(0xAAAAAA);
+    scene.add(ambientLight);
+
+    const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+    scene.add(hemisphereLight);
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+    directionalLight.position.set(100, 100, 100);
+    scene.add(directionalLight);
+
+    const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
+    // scene.add(directionalLightHelper);
+
+    const gridHelper = new THREE.GridHelper(50, 20);
+    scene.add(gridHelper);
+
+    const axesHelper = new THREE.AxesHelper(5);
+    scene.add(axesHelper);
+
+    window.addEventListener('resize', onResize);
+    onResize();
+}
+
+const steamChamberLen = 20;
+const steamChamberRad = 5;
+const steamChamberEndRad = steamChamberRad+0.75;
+const steamChamberEndLen = 5;
+const cabinLen = 10;
+const cabinHeight = 11;
+const cabinRoofHeight = 8;
+const cabinWallThickness = 0.75;
+const wheelRad = 2.75;
+const chassisHeight = 5;
+const wheelThickness = 0.85;
+const chassisOffset = 2.49;
+const wheelOffset = -0.70;
+const steamCylindersLen = 8;
+const crankLen = 22;
+const crankOffset = 3.750;
+const crankWidth = 0.5;
+
+let crankLeft, crankRight;
+
+function buildCabinRoof() {
+    console.log('Building train cabin roof');
+    const geometry = new THREE.BoxGeometry(12, cabinWallThickness, 12);
+    return geometry;
+}
+
+function buildCabin() {
+    console.log('Building train cabin');
+
+    let cabin = [];
+
+    const cabinFront = new THREE.BoxGeometry(steamChamberRad*2, cabinWallThickness, cabinHeight);
+    cabinFront.translate(0, cabinLen/2, -cabinHeight/2);
+    cabin.push(cabinFront);
+
+    const cabinLeft = new THREE.BoxGeometry(steamChamberRad*2, cabinWallThickness, cabinHeight);
+    cabinLeft.rotateZ(Math.PI/2);
+    cabinLeft.translate(steamChamberRad-cabinWallThickness/2, cabinWallThickness/2, -cabinHeight/2);
+    cabin.push(cabinLeft);
+
+    const cabinRight = new THREE.BoxGeometry(steamChamberRad*2, cabinWallThickness, cabinHeight);
+    cabinRight.rotateZ(Math.PI/2);
+    cabinRight.translate(-steamChamberRad+cabinWallThickness/2, cabinWallThickness/2, -cabinHeight/2);
+    cabin.push(cabinRight);
+
+    const g1 = new THREE.BoxGeometry(cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+    g1.rotateZ(Math.PI/2);
+    g1.translate(-steamChamberRad+(cabinWallThickness/2), -steamChamberRad+cabinWallThickness, -cabinHeight-cabinRoofHeight/2);
+    cabin.push(g1);
+
+    const g2 = new THREE.BoxGeometry(cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+    g2.rotateZ(Math.PI/2);
+    g2.translate(steamChamberRad-cabinWallThickness/2, steamChamberRad, -cabinHeight-cabinRoofHeight/2);
+    cabin.push(g2);
+
+    const g3 = new THREE.BoxGeometry(cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+    g3.rotateZ(Math.PI/2);
+    g3.translate(steamChamberRad-cabinWallThickness/2, -steamChamberRad+cabinWallThickness, -cabinHeight-cabinRoofHeight/2);
+    cabin.push(g3);
+
+    const g4 = new THREE.BoxGeometry(cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+    g4.rotateZ(Math.PI/2);
+    g4.translate(-steamChamberRad+cabinWallThickness/2, steamChamberRad, -cabinHeight-cabinRoofHeight/2);
+    cabin.push(g4);
+
+    const geometry = BufferGeometryUtils.mergeGeometries(cabin);
+    geometry.rotateX(Math.PI/2);
+    return geometry;
+}
+
+function buildChamber() {
+    let geometries = [];
+
+    const steamChamber = new THREE.CylinderGeometry(steamChamberRad,
+        steamChamberRad, steamChamberLen, 32);
+    
+    geometries.push(steamChamber);
+
+    const steamChamberEnd = new THREE.CylinderGeometry(steamChamberEndRad,
+        steamChamberEndRad, steamChamberEndLen, 32);
+
+    steamChamberEnd.translate(0,steamChamberLen/2 + steamChamberEndLen/2,0);
+    geometries.push(steamChamberEnd);
+
+    const floor = new THREE.BoxGeometry(steamChamberRad*2, steamChamberLen + steamChamberEndLen + cabinLen, 1.0);
+    floor.translate(0, -steamChamberEndLen/2, steamChamberRad);
+    geometries.push(floor);
+
+    const chamberPipeLen = 8;
+    const chamberPipe = new THREE.CylinderGeometry(0.75, 0.75, chamberPipeLen, 32);
+    chamberPipe.translate(0, -(steamChamberRad + chamberPipeLen/2)+1.0,
+        -(steamChamberLen+steamChamberEndLen)/2);
+    chamberPipe.rotateX(Math.PI/2);
+    geometries.push(chamberPipe);
+
+    const geometry = BufferGeometryUtils.mergeGeometries(geometries);
+    geometry.rotateX(Math.PI/2);
+    geometry.translate(0, steamChamberRad, 0);
+    return geometry;
+}
+
+function buildTrainWheel() {
+    const wheel = new THREE.CylinderGeometry(wheelRad, wheelRad, wheelThickness);
+    wheel.rotateZ(Math.PI/2);
+
+    const wheelBolt = new THREE.CylinderGeometry(wheelRad, wheelRad, wheelThickness);
+    wheelBolt.rotateZ(Math.PI/2);
+
+    const wheelsMaterial = new THREE.MeshPhongMaterial({
+        color: 0x393939, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    return new THREE.Mesh(wheel, wheelsMaterial)
+}
+
+function buildTrainAxe(material) {
+    const axeGeometry = new THREE.CylinderGeometry(0.65, 0.65, 10);
+    axeGeometry.rotateZ(Math.PI/2);
+
+    const axeMaterial = new THREE.MeshPhongMaterial({
+        color: 0x7A7F80, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    return new THREE.Mesh(axeGeometry, axeMaterial);
+}
+
+function buildTrainChassis() {
+    const chassis = new THREE.BoxGeometry(7, 5, steamChamberLen+steamChamberEndLen+cabinLen);
+    return chassis;
+}
+
+function buildTrain() {
+    console.log('Building train');
+    const train = new THREE.Group();
+
+    const chassisGeometry = buildTrainChassis();
+    const chassisMaterial = new THREE.MeshPhongMaterial({
+        color: 0x7A7F80, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    const chassis = new THREE.Mesh(chassisGeometry, chassisMaterial);
+    train.add(chassis);
+
+    const chamberGeometry = buildChamber();
+    const chamberMaterial = new THREE.MeshPhongMaterial({
+        color: 0xFA1A09, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    const chamber = new THREE.Mesh(chamberGeometry, chamberMaterial);
+    chassis.add(chamber);
+    chamber.position.set(0, (chassisHeight + cabinWallThickness)/2, chassisOffset);
+
+    const cabinGeometry = buildCabin();
+    const cabin = new THREE.Mesh(cabinGeometry, chamberMaterial);
+    chassis.add(cabin);
+    cabin.position.set(0, (chassisHeight + cabinWallThickness)/2, -steamChamberLen+(cabinLen/2)+chassisOffset);
+
+    const cabinRoofGeometry = buildCabinRoof();
+    const roofMaterial = new THREE.MeshPhongMaterial({
+        color: 0xFBEC50, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    const cabinRoof = new THREE.Mesh(cabinRoofGeometry, roofMaterial);
+    cabin.add(cabinRoof);
+    cabinRoof.position.set(0, cabinHeight+cabinRoofHeight+cabinWallThickness/2, 0);
+
+    const a1 = buildTrainAxe();
+    chassis.add(a1);
+
+    const a2 = buildTrainAxe();
+    chassis.add(a2);
+
+    const a3 = buildTrainAxe(chassisMaterial);
+    chassis.add(a3);
+
+    a1.position.set(0, wheelOffset, 0);
+    a2.position.set(0, wheelOffset, wheelRad*2.5);
+    a3.position.set(0, wheelOffset, -wheelRad*2.5);
+
+
+    const cylinderLeft = new THREE.CylinderGeometry(2.25, 2.5, steamCylindersLen);
+    cylinderLeft.rotateX(Math.PI/2);
+    cylinderLeft.translate(steamChamberRad-0.25, 0, steamChamberLen-steamCylindersLen/1.5);
+
+    const cylinderRight = new THREE.CylinderGeometry(2.25, 2.5, steamCylindersLen);
+    cylinderRight.rotateX(Math.PI/2);
+    cylinderRight.translate(-steamChamberRad+0.25, 0, steamChamberLen-steamCylindersLen/1.5);
+
+    const cylindersGeometry = BufferGeometryUtils.mergeGeometries([cylinderRight, cylinderLeft]);
+    const cylindersMaterial = new THREE.MeshPhongMaterial({
+        color: 0x393939, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    chassis.add(new THREE.Mesh(cylindersGeometry, cylindersMaterial));
+    chassis.position.set(0,-2,-2.75);
+
+    const w1 = buildTrainWheel();
+    w1.position.set(steamChamberRad-wheelThickness/2.1,0,0);
+    a1.add(w1);
+
+    const w2 = buildTrainWheel();
+    w2.position.set(-steamChamberRad+wheelThickness/2.1,0,0);
+    a1.add(w2);
+
+    const w3 = buildTrainWheel();
+    w3.position.set(steamChamberRad-wheelThickness/2.1,0,0);
+    a2.add(w3);
+
+    const w4 = buildTrainWheel();
+    w4.position.set(-steamChamberRad+wheelThickness/2.1,0,);
+    a2.add(w4);
+
+    const w5 = buildTrainWheel();
+    w5.position.set(steamChamberRad-wheelThickness/2.1,0,0);
+    a3.add(w5);
+
+    const w6 = buildTrainWheel();
+    w6.position.set(-steamChamberRad+wheelThickness/2.1,0,0);
+    a3.add(w6);
+
+    const crankGeometry = new THREE.BoxGeometry(crankWidth, 1.0, crankLen);
+
+    crankRight = new THREE.Mesh(crankGeometry, chassisMaterial);
+    //crankRight.position.set(steamChamberRad, wheelOffset, crankOffset);
+
+    crankLeft = new THREE.Mesh(crankGeometry, chassisMaterial);
+    //crankLeft.position.set(-steamChamberRad, wheelOffset, crankOffset);
+
+    chassis.add(crankLeft);
+    chassis.add(crankRight);
+
+    chassis.translateY(-wheelOffset);
+
+    train.position.set(0, 2, 0);
+    return train;
+}
+
+function buildScene() {
+    console.log('Building scene');
+
+    const train = buildTrain();
+    scene.add(train);
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function mainLoop() {
+    time += 0.05;
+
+    requestAnimationFrame(mainLoop);
+
+    crankLeft.position.set(-steamChamberRad-crankWidth/2,
+        wheelOffset+1.00*(Math.sin(time*Math.PI/2)),
+        crankOffset - 1.00*(Math.cos(time*Math.PI/2)));
+
+    crankRight.position.set(steamChamberRad+crankWidth/2,
+        wheelOffset+1.00*(Math.sin(time*Math.PI/2)),
+        crankOffset - 1.00*(Math.cos(time*Math.PI/2)));
+
+    renderer.render(scene, camera);
+}
+
+function main() {
+    buildScene();
+    mainLoop();
+}
+
+setupThreeJs();
+main();
diff --git a/tp/src/standalone/trees.js b/tp/src/standalone/trees.js
@@ -0,0 +1,204 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { vertexShader, fragmentShader } from '/assets/treesShaders.js';
+
+let scene, camera, renderer, container, terrainMaterial, instancedTrees;
+
+const textures = {
+    tierra: { url: '/assets/tierra.jpg', object: null },
+    roca: { url: '/assets/roca.jpg', object: null },
+    pasto: { url: '/assets/pasto.jpg', object: null },
+};
+
+function onResize() {
+    camera.aspect = container.offsetWidth / container.offsetHeight;
+    camera.updateProjectionMatrix();
+    renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+    scene = new THREE.Scene();
+    container = document.getElementById('mainContainer');
+
+    renderer = new THREE.WebGLRenderer();
+    renderer.setClearColor(0x606060);
+    container.appendChild(renderer.domElement);
+
+    camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000);
+    camera.position.set(-50, 60, 50);
+    camera.lookAt(0, 0, 0);
+
+    const controls = new OrbitControls(camera, renderer.domElement);
+
+    const ambientLight = new THREE.AmbientLight(0xffffff);
+    scene.add(ambientLight);
+
+    const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+    scene.add(hemisphereLight);
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+    directionalLight.position.set(100, 100, 100);
+    scene.add(directionalLight);
+
+    const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
+    // scene.add(directionalLightHelper);
+
+    //const gridHelper = new THREE.GridHelper(50, 20);
+    //scene.add(gridHelper);
+
+    const axesHelper = new THREE.AxesHelper(5);
+    scene.add(axesHelper);
+
+    window.addEventListener('resize', onResize);
+    onResize();
+}
+
+function createInstancedTrees(count) {
+    console.log('Generating `' + count + '` instances of tree');
+
+    let logHeight = 4.0;
+
+    const treeLogGeometry    = new THREE.CylinderGeometry(0.30, 0.30, logHeight, 40, 40);
+    const treeLeavesGeometry = new THREE.SphereGeometry(1.75,40,40);
+
+    treeLogGeometry.translate(0, logHeight/2.0, 0);
+
+    const instancedTreeLogGeometry = new THREE.InstancedBufferGeometry();
+    instancedTreeLogGeometry.copy(treeLogGeometry);
+
+    const instancedTreeLeavesGeometry = new THREE.InstancedBufferGeometry();
+    instancedTreeLeavesGeometry.copy(treeLeavesGeometry);
+
+    const treeLogMaterial   = new THREE.MeshPhongMaterial({color: 0x7c3f00});
+    const instancedTreeLogs = new THREE.InstancedMesh(instancedTreeLogGeometry, treeLogMaterial, count);
+
+    const treeLeavesMaterial  = new THREE.MeshPhongMaterial({color: 0x365829});
+    const instancedTreeLeaves = new THREE.InstancedMesh(instancedTreeLeavesGeometry, treeLeavesMaterial, count);
+
+    const rotMatrix = new THREE.Matrix4();
+
+    const translationMatrix = new THREE.Matrix4();
+    const treeLogMatrix    = new THREE.Matrix4();
+    const treeLeavesMatrix = new THREE.Matrix4();
+
+    //let origin = new THREE.Vector3();
+    const RANGE = 50 - 4/2;
+
+    for (let i = 0; i < count; i++) {
+        let position = new THREE.Vector3(
+            (Math.random() - 0.5) * RANGE,
+            0,
+            (Math.random() - 0.5) * RANGE
+        );
+
+        translationMatrix.makeTranslation(position);
+
+        //rotMatrix.lookAt(0, 0, new THREE.Vector3(0, 1, 0));
+        treeLogMatrix.identity();
+        treeLeavesMatrix.identity();
+
+        let scale = 0.5 + (Math.random()*(logHeight/3));
+        treeLogMatrix.makeScale(1, scale, 1);
+        //matrix.premultiply(rotMatrix);
+
+        treeLogMatrix.premultiply(translationMatrix);
+
+        position.y = scale*logHeight;
+        translationMatrix.makeTranslation(position);
+        treeLeavesMatrix.premultiply(translationMatrix);
+
+        instancedTreeLogs.setMatrixAt(i, treeLogMatrix);
+        instancedTreeLeaves.setMatrixAt(i, treeLeavesMatrix);
+    }
+
+    scene.add(instancedTreeLogs);
+    scene.add(instancedTreeLeaves);
+}
+
+function buildScene() {
+    console.log('Building scene');
+
+    console.log('Generating terrain');
+    const terrainGeometry = new THREE.PlaneGeometry(50, 50);
+    //const terrainMaterial = new THREE.MeshPhongMaterial( {color: 0x365829, side: THREE.DoubleSide} );
+    terrainMaterial = new THREE.RawShaderMaterial({
+        uniforms: {
+            tierraSampler: { type: 't', value: textures.tierra.object },
+            rocaSampler: { type: 't', value: textures.roca.object },
+            pastoSampler: { type: 't', value: textures.pasto.object },
+            scale1: { type: 'f', value: 2.0 },
+
+            mask1low: { type: 'f', value: -0.38 },
+            mask1high: { type: 'f', value: 0.1 },
+
+            mask2low: { type: 'f', value: 0.05 },
+            mask2high: { type: 'f', value: -0.70 },
+        },
+        vertexShader: vertexShader,
+        fragmentShader: fragmentShader,
+        side: THREE.DoubleSide,
+    });
+    terrainMaterial.needsUpdate = true;
+
+    const terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
+    terrain.rotateX(Math.PI/2);
+    terrain.position.set(0, 0, 0);
+    scene.add(terrain);
+
+    console.log('Generating trees');
+    createInstancedTrees(35);
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+
+function createMenu() {
+    const gui = new dat.GUI({ width: 400 });
+    gui.add(terrainMaterial.uniforms.scale1, 'value', 0, 10).name('Texture scale');
+    gui.add(terrainMaterial.uniforms.mask1low, 'value', -1, 1).name('Mask1 Low');
+    gui.add(terrainMaterial.uniforms.mask1high, 'value', -1, 1).name('Mask1 High');
+    gui.add(terrainMaterial.uniforms.mask2low, 'value', -1, 1).name('Mask2 Low');
+    gui.add(terrainMaterial.uniforms.mask2high, 'value', -1, 1).name('Mask2 High');
+}
+
+function mainLoop() {
+    requestAnimationFrame(mainLoop);
+    renderer.render(scene, camera);
+}
+
+function main() {
+    buildScene();
+    createMenu();
+    mainLoop();
+}
+
+setupThreeJs();
+loadTextures(main);
diff --git a/tp/src/standalone/tunnel.js b/tp/src/standalone/tunnel.js
@@ -0,0 +1,166 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+
+let scene, camera, renderer, container;
+
+const textures = {
+    madera: { url: '/assets/madera.jpg', object: null },
+};
+
+function onResize() {
+    camera.aspect = container.offsetWidth / container.offsetHeight;
+    camera.updateProjectionMatrix();
+    renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+    scene = new THREE.Scene();
+    container = document.getElementById('mainContainer');
+
+    renderer = new THREE.WebGLRenderer();
+    renderer.setClearColor(0x606060);
+    container.appendChild(renderer.domElement);
+
+    camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000);
+    camera.position.set(-50, 60, 50);
+    camera.lookAt(0, 0, 0);
+
+    const controls = new OrbitControls(camera, renderer.domElement);
+
+    const ambientLight = new THREE.AmbientLight(0xffffff);
+    scene.add(ambientLight);
+
+    const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+    scene.add(hemisphereLight);
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+    directionalLight.position.set(100, 100, 100);
+    scene.add(directionalLight);
+
+    const gridHelper = new THREE.GridHelper(50, 20);
+    scene.add(gridHelper);
+
+    const axesHelper = new THREE.AxesHelper(5);
+    scene.add(axesHelper);
+
+    window.addEventListener('resize', onResize);
+    onResize();
+}
+
+function buildScene() {
+    console.log('Building scene');
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+function generateTunnel() {
+    const tunnelHeight        = 20;
+    const tunnelWidth         = 14;
+    const tunnelWallThickness = 0.5;
+    const tunnelLen           = 26;
+
+    const path = new THREE.Path();
+    path.moveTo(-tunnelWidth/2, 0);
+    path.lineTo(-tunnelWidth/2, tunnelHeight*1/3);
+    path.moveTo(-tunnelWidth/2, tunnelHeight*1/3);
+    path.quadraticCurveTo(0, tunnelHeight, tunnelWidth/2, tunnelHeight*1/3);
+    path.moveTo(tunnelWidth/2, 0);
+    path.lineTo(tunnelWidth/2, 0);
+
+
+    // cerramos la curva con otra de la misma forma con una diferencia de
+    // `tunnelWallThickness`
+    path.lineTo(tunnelWidth/2-tunnelWallThickness, 0);
+    path.moveTo(tunnelWidth/2-tunnelWallThickness, 0);
+
+    path.lineTo(tunnelWidth/2-tunnelWallThickness, tunnelHeight*1/3);
+    path.moveTo(tunnelWidth/2-tunnelWallThickness, tunnelHeight*1/3);
+
+    path.quadraticCurveTo(
+        0, tunnelHeight-(tunnelWallThickness*2),
+        -tunnelWidth/2+tunnelWallThickness, tunnelHeight*1/3);
+
+    path.lineTo(-tunnelWidth/2+tunnelWallThickness, 0);
+    path.moveTo(-tunnelWidth/2+tunnelWallThickness, 0);
+
+    path.lineTo(-tunnelWidth/2, 0);
+    path.moveTo(-tunnelWidth/2, 0);
+
+    const points = path.getPoints();
+
+    /*
+    // muestra la curva utilizada para la extrusión
+    const geometry = new THREE.BufferGeometry().setFromPoints(points);
+    const lineMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
+    const curveObject = new THREE.Line(geometry, lineMaterial);
+    scene.add(curveObject);
+    */
+
+    const shape = new THREE.Shape(points);
+
+    const extrudeSettings = {
+        curveSegments: 24,
+        steps: 50,
+        depth: tunnelLen,
+    };
+
+    const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
+    geometry.translate(0, 0, -tunnelLen/2);
+
+    textures.madera.object.wrapS = THREE.RepeatWrapping;
+    textures.madera.object.wrapT = THREE.RepeatWrapping;
+    textures.madera.object.repeat.set(0.10, 0.10);
+    textures.madera.object.anisotropy = 16;
+
+    const tunnelMaterial = new THREE.MeshPhongMaterial({
+        side: THREE.DoubleSide,
+        transparent: false,
+        opacity: 1.0,
+        shininess: 10,
+        map: textures.madera.object
+    });
+
+    const mesh = new THREE.Mesh(geometry, tunnelMaterial) ;
+    scene.add(mesh);
+}
+
+function mainLoop() {
+    requestAnimationFrame(mainLoop);
+    renderer.render(scene, camera);
+}
+
+function main() {
+    generateTunnel();
+    mainLoop();
+}
+
+setupThreeJs();
+loadTextures(main);
diff --git a/tp/src/terrain.js b/tp/src/terrain.js
@@ -0,0 +1,170 @@
+import * as THREE from 'three';
+import { vertexShader, fragmentShader } from '/assets/shaders.js';
+
+let terrainMaterial, terrainGeometry, terrain;
+
+const widthSegments   = 100;
+const heightSegments  = 100;
+const amplitude       = 8;
+const amplitudeBottom = -1.00;
+
+const textures = {
+    tierra: { url: '/assets/tierra.jpg', object: null },
+    roca: { url: '/assets/roca.jpg', object: null },
+    pasto: { url: '/assets/pasto.jpg', object: null },
+    elevationMap: { url: '/assets/elevation_map2.png', object: null },
+};
+
+// La funcion devuelve una geometria de Three.js
+// width: Ancho del plano
+// height: Alto del plano
+// amplitude: Amplitud de la elevacion
+// widthSegments: Numero de segmentos en el ancho
+// heightSegments: Numero de segmentos en el alto
+// texture: Textura que se usara para la elevacion
+export function elevationGeometry(width, height, amplitude, widthSegments, heightSegments, texture) {
+    console.log('Generating terrain geometry');
+    let geometry = new THREE.BufferGeometry();
+
+    const positions = [];
+    const indices = [];
+    const normals = [];
+    const uvs = [];
+
+    // Creamos un canvas para poder leer los valores de los píxeles de la textura
+    let canvas = document.createElement('canvas');
+    let ctx = canvas.getContext('2d');
+    let img = texture.image;
+
+    // Ajustamos el tamaño del canvas segun la cantidad de segmentos horizontales y verticales
+    canvas.width = widthSegments;
+    canvas.height = heightSegments;
+
+    // Dibujamos la textura en el canvas en la escala definida por widthSegments y heightSegments
+    ctx.drawImage(img, 0, 0, widthSegments, heightSegments);
+
+    // Obtenemos los valores de los píxeles de la textura
+    let imageData = ctx.getImageData(0, 0, widthSegments, heightSegments);
+    let data = imageData.data; // Este es un array con los valores de los píxeles
+
+    const quadsPerRow = widthSegments - 1;
+
+    // Recorremos los segmentos horizontales y verticales
+    for (let i = 0; i < widthSegments - 1; i++) {
+        for (let j = 0; j < heightSegments - 1; j++) {
+            // Obtenemos los valores de los píxeles de los puntos adyacentes
+            let xPrev = undefined;
+            let xNext = undefined;
+            let yPrev = undefined;
+            let yNext = undefined;
+
+            // Obtenemos el valor del pixel en la posicion i, j
+            // console.log('getting elevation map value at: (' + i + ',' + j + ')');
+            let z0 = data[(i + j * widthSegments) * 4] / 255;
+
+            // Obtenemos los valores de los píxeles adyacentes
+            xPrev = i > 0 ? data[(i - 1 + j * widthSegments) * 4] / 255 : undefined;
+            xNext = i < widthSegments - 1 ? (xNext = data[(i + 1 + j * widthSegments) * 4] / 255) : undefined;
+
+            yPrev = j > 0 ? data[(i + (j - 1) * widthSegments) * 4] / 255 : undefined;
+            yNext = j < heightSegments - 1 ? data[(i + (j + 1) * widthSegments) * 4] / 255 : undefined;
+
+            // calculamos la diferencia entre los valores de los píxeles adyacentes
+            // en el eje `x` y en el eje `y` de la imagen (en el espacio de la textura
+            // Ojo no confundir con el espacio 3D del modelo 3D donde Y es la altura)
+            let deltaX;
+            if (xPrev == undefined) {
+                deltaX = xNext - z0;
+            } else if (yNext == undefined) {
+                deltaX = xPrev - z0;
+            } else {
+                deltaX = (xNext - xPrev) / 2;
+            }
+
+            let deltaY;
+            if (yPrev == undefined) {
+                deltaY = yNext - z0;
+            } else if (yNext == undefined) {
+                deltaY = yPrev - z0;
+            } else {
+                deltaY = (yNext - yPrev) / 2;
+            }
+
+            // Calculamos la altura del punto en el espacio 3D
+            const z = amplitude * z0;
+
+            // Añadimos los valores de los puntos al array de posiciones
+            positions.push((width * i) / widthSegments - width / 2);
+            positions.push(z);
+            positions.push((height * j) / heightSegments - height / 2);
+
+            // Calculamos los vectores tangentes a la superficie en el ejex y en el eje y
+            let tanX = new THREE.Vector3(width / widthSegments, deltaX * amplitude, 0).normalize();
+            let tanY = new THREE.Vector3(0, deltaY * amplitude, height / heightSegments).normalize();
+
+            // Calculamos el vector normal a la superficie
+            let n = new THREE.Vector3();
+            n.crossVectors(tanY, tanX);
+
+            // Añadimos los valores de los vectores normales al array de normales
+            normals.push(n.x);
+            normals.push(n.y);
+            normals.push(n.z);
+
+            uvs.push(i / (widthSegments - 1));
+            uvs.push(j / (heightSegments - 1));
+
+            if (i == widthSegments - 2 || j == heightSegments - 2) continue;
+
+            // Ensamblamos los triangulos
+            indices.push(i + j * quadsPerRow);
+            indices.push(i + 1 + j * quadsPerRow);
+            indices.push(i + 1 + (j + 1) * quadsPerRow);
+
+            indices.push(i + j * quadsPerRow);
+            indices.push(i + 1 + (j + 1) * quadsPerRow);
+            indices.push(i + (j + 1) * quadsPerRow);
+        }
+    }
+
+    geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
+    geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
+    geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
+    geometry.setIndex(indices);
+
+    return geometry;
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+function main() {
+}
+
+loadTextures(main);
diff --git a/tp/src/track-map.js b/tp/src/track-map.js
@@ -0,0 +1,357 @@
+import * as THREE from 'three';
+
+let treesForbiddenMapData, treesForbiddenMap, elevationMap, elevationMapData;
+
+const textures = {
+    elevationMap:     { url: '/assets/elevation_map2.png', object: null },
+    treeForbiddenMap: { url: '/assets/tree_forbidden_zone_map.png', object: null }
+};
+
+const widthSegments   = 100;
+const heightSegments  = 100;
+const amplitude       = 10;
+const amplitudeBottom = -1.00;
+const imgWidth  = 512;
+const imgHeight = 512;
+
+function getPixel(imgData, index) {
+  let i = index*4, d = imgData.data
+  return [d[i],d[i+1],d[i+2],d[i+3]] // Returns array [R,G,B,A]
+}
+
+function getPixelXY(imgData, x, y) {
+  return getPixel(imgData, y*imgData.width+x)
+}
+
+// position: Vector3
+function isForbbidenPosition(position) {
+    const x = Math.floor(position.x);
+    const y = position.y;
+    const z = Math.floor(position.z);
+
+    // TODO: estos valores deberian depender de la posicion del terreno
+    if((y > 5.8) || (y < 3.25)) {
+        // console.log("(" + position.x + ", " + position.y + ", " + position.z + ") is not valid ");
+        return true;
+    }
+    
+    let pixelArray = getPixelXY(treesForbiddenMap, x, z);
+    const R = pixelArray[0]; // Red
+    const G = pixelArray[1]; // Green
+    const B = pixelArray[2]; // Blue
+    const A = pixelArray[3]; // Alpha
+    // const pixel = new THREE.Vector4(R, G, B, A);
+
+    if(((R <= 10) && (G >= 250) && (B <= 10))
+        || (R <= 80) && (G <= 80) && (B <= 80)
+        || (R >= 200) && (G >= 200) && (B >= 200)) {
+        // console.log("(" + position.x + ", " + position.y + ", " + position.z + ") is not valid ");
+        return true;
+    }
+
+    // console.log("(" + position.x + ", " + position.y + ") is valid ");
+    return false;
+}
+
+// obtiene una posicion aleatoria en el terreno, para obtener la altura del
+// terreno utiliza el mapa de elevacion.
+// `padding` permite definir un borde del cual no se toman puntos
+function getRandomPositionInTerrain(padding = 0) {
+    const x = Math.floor(Math.random() * (widthSegments-(padding*2)));
+    const z = Math.floor(Math.random() * (heightSegments-(padding*2)));
+
+    const pixelArray = getPixelXY(elevationMap, x, z); // array [R,G,B,A]
+    const y = (pixelArray[0]/255)*amplitude;
+
+    const position = new THREE.Vector3(x+padding, y, z+padding);
+    return position;
+}
+
+// La funcion devuelve una geometria de Three.js
+// width: Ancho del plano
+// height: Alto del plano
+// amplitude: Amplitud de la elevacion
+// widthSegments: Numero de segmentos en el ancho
+// heightSegments: Numero de segmentos en el alto
+function elevationGeometry(width, height, amplitude, widthSegments, heightSegments) {
+    console.log('Generating terrain geometry');
+    let geometry = new THREE.BufferGeometry();
+
+    const positions = [];
+    const indices = [];
+    const normals = [];
+    const uvs = [];
+
+    let imageData = elevationMap;
+    let data = elevationMapData;
+
+    const quadsPerRow = widthSegments - 1;
+
+    // Recorremos los segmentos horizontales y verticales
+    for (let x = 0; x < widthSegments - 1; x++) {
+        for (let y = 0; y < heightSegments - 1; y++) {
+            // valor del pixel en el mapa de elevacion en la posicion i, j
+
+            let pixel = getPixelXY(imageData, x, y);
+
+            // se utiliza el canal rojo [R, G, B, A];
+            let z0 = pixel[0] / 255;
+            const z = amplitude * z0;
+
+            // valores de los píxeles de los puntos adyacentes
+            let xPrev, xNext, yPrev, yNext;
+
+            xPrev = (x > 0) ? getPixelXY(imageData, x-1, y)[0]/255 : undefined;
+            xNext = (x < widthSegments-1) ? getPixelXY(imageData, x+1, y)[0]/255 : undefined;
+
+            yPrev = (y > 0) ? getPixelXY(imageData, x, y+1)[0]/255 : undefined;
+            yNext = (y < heightSegments-1) ? getPixelXY(imageData, x, y+1)[0]/255 : undefined;
+
+            // diferencia entre los valores de los píxeles adyacentes en el eje
+            // `x` y en el eje `y` de la imagen en el espacio de la textura
+            let deltaX;
+            if (xPrev == undefined) {
+                deltaX = xNext - z0;
+            } else if (yNext == undefined) {
+                deltaX = xPrev - z0;
+            } else {
+                deltaX = (xNext - xPrev) / 2;
+            }
+
+            let deltaY;
+            if (yPrev == undefined) {
+                deltaY = yNext - z0;
+            } else if (yNext == undefined) {
+                deltaY = yPrev - z0;
+            } else {
+                deltaY = (yNext - yPrev) / 2;
+            }
+
+            // Añadimos los valores de los puntos al array de posiciones
+            positions.push((width * x) / widthSegments - width / 2);
+            positions.push(z);
+            positions.push((height * y) / heightSegments - height / 2);
+
+            // vectores tangentes a la superficie en el eje `x` y en el eje `y`
+            let tanX = new THREE.Vector3(width / widthSegments, deltaX * amplitude, 0);
+            let tanY = new THREE.Vector3(0, deltaY * amplitude, height / heightSegments);
+
+            tanX.normalize();
+            tanY.normalize();
+
+            let normal = new THREE.Vector3().crossVectors(tanY, tanX);
+
+            normals.push(normal.x);
+            normals.push(normal.y);
+            normals.push(normal.z);
+
+            uvs.push(x / (widthSegments - 1));
+            uvs.push(y / (heightSegments - 1));
+
+            if ((x == widthSegments-2) || (y == heightSegments-2)) continue;
+
+            // Ensamblamos los triangulos
+            indices.push(x + y*quadsPerRow);
+            indices.push(x + 1 + y*quadsPerRow);
+            indices.push(x + 1 + (y+1)*quadsPerRow);
+
+            indices.push(x + y*quadsPerRow);
+            indices.push(x + 1 + (y+1)*quadsPerRow);
+            indices.push(x + (y+1)*quadsPerRow);
+        }
+    }
+
+    geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
+    geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
+    geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
+    geometry.setIndex(indices);
+
+    return geometry;
+}
+
+// devuelve un arreglo de 2 `instancedMesh` con los troncos y copas de los arboles
+export function createInstancedTrees(count) {
+    console.log('Generating `' + count + '` instances of tree');
+
+    let logHeight = 3.0;
+    const treeLogGeometry   = new THREE.CylinderGeometry(
+        0.10, 0.25, logHeight, 40, 40);
+    treeLogGeometry.translate(0, logHeight/2.0, 0);
+    const instancedTreeLogGeometry = new THREE.InstancedBufferGeometry();
+    instancedTreeLogGeometry.copy(treeLogGeometry);
+    const treeLogMaterial   = new THREE.MeshPhongMaterial({color: 0x7c3f00});
+    const instancedTreeLogs = new THREE.InstancedMesh(
+        instancedTreeLogGeometry,
+        treeLogMaterial,
+        count);
+
+    const treeLeavesRadius = 1.25;
+    const treeLeavesGeometry = new THREE.SphereGeometry(treeLeavesRadius,40,40);
+    const instancedTreeLeavesGeometry = new THREE.InstancedBufferGeometry();
+    instancedTreeLeavesGeometry.copy(treeLeavesGeometry);
+    const treeLeavesMaterial  = new THREE.MeshPhongMaterial({color: 0x365829});
+    const instancedTreeLeaves = new THREE.InstancedMesh(
+        instancedTreeLeavesGeometry,
+        treeLeavesMaterial,
+        count);
+
+    const rotMatrix         = new THREE.Matrix4();
+    const translationMatrix = new THREE.Matrix4();
+    const treeLogMatrix     = new THREE.Matrix4();
+    const treeLeavesMatrix  = new THREE.Matrix4();
+
+    const treesBorderPadding = 3.0;
+    for (let i = 0; i < count; i++) {
+        let position = getRandomPositionInTerrain(treesBorderPadding);
+        for(let j = 0; isForbbidenPosition(position); ++j) {
+            position = getRandomPositionInTerrain(treesBorderPadding);
+            if(j++ == 1000) { // maximo de iteraciones
+                break;
+            }
+        }
+
+        if(isForbbidenPosition(position)) {
+            continue;
+        }
+
+        const treeOffset = -1.50;
+
+        // 1.50 numbero magico para posicionar correctamente los arboles con
+        // respecto al terreno
+        position.x -= (widthSegments+treesBorderPadding+1.50)/2;
+        position.y += (amplitudeBottom + treeOffset);
+        position.z -= (heightSegments+treesBorderPadding)/2;
+        translationMatrix.makeTranslation(position);
+        treeLogMatrix.identity();
+        treeLeavesMatrix.identity();
+
+        let scale = 0.6 + (Math.random()*(logHeight/3));
+        treeLogMatrix.makeScale(1, scale, 1);
+        treeLogMatrix.premultiply(translationMatrix);
+
+        position.y += scale*logHeight;
+
+        translationMatrix.makeTranslation(position);
+        treeLeavesMatrix.premultiply(translationMatrix);
+
+        instancedTreeLogs.setMatrixAt(i, treeLogMatrix);
+        instancedTreeLeaves.setMatrixAt(i, treeLeavesMatrix);
+    }
+
+    console.log('Done generating `' + count + '` instances of tree');
+    return [instancedTreeLogs, instancedTreeLeaves];
+}
+
+
+
+function buildTrees() {
+    console.log('Building scene');
+
+    const width  = widthSegments;
+    const height = heightSegments;
+
+    terrainGeometry = elevationGeometry(
+        width, height,
+        amplitude,
+        widthSegments, heightSegments);
+
+    console.log('Applying textures');
+    terrainMaterial = new THREE.RawShaderMaterial({
+        uniforms: {
+            dirtSampler: { type: 't', value: textures.tierra.object },
+            rockSampler: { type: 't', value: textures.roca.object },
+            grassSampler: { type: 't', value: textures.pasto.object },
+            scale: { type: 'f', value: 3.0 },
+            terrainAmplitude: { type: 'f', value: amplitude },
+            terrainAmplitudeBottom: { type: 'f', value: amplitudeBottom },
+            worldNormalMatrix: { type: 'm4', value: null },
+            dirtStepWidth: { type: 'f', value: 0.20 },
+            rockStepWidth: { type: 'f', value: 0.15 },
+        },
+        vertexShader: vertexShader,
+        fragmentShader: fragmentShader,
+        side: THREE.DoubleSide,
+    });
+    terrainMaterial.needsUpdate = true;
+
+    terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
+    terrain.position.set(0, amplitudeBottom, 0);
+    scene.add(terrain);
+
+    const waterGeometry = new THREE.PlaneGeometry(width/2, height-1.55);
+    const waterMaterial = new THREE.MeshPhongMaterial( {color: 0x12ABFF, side: THREE.DoubleSide} );
+    const water = new THREE.Mesh( waterGeometry, waterMaterial );
+    water.rotateX(Math.PI/2);
+    water.position.set(0, 0.75, -1.00);
+    // scene.add(water);
+
+    const [treeLogs, treeLeaves] = createInstancedTrees(250);
+    return 
+    // scene.add(treeLogs);
+    // scene.add(treeLeaves);
+}
+
+function loadMapsData() {
+    console.log("Loading maps data");
+
+    // Creamos un canvas para poder leer los valores de los píxeles de la textura
+    let canvas = document.createElement('canvas');
+    let ctx = canvas.getContext('2d');
+
+    let treesForbiddenMapImage = textures.treeForbiddenMap.object.image;
+    let elevationMapImage = textures.elevationMap.object.image;
+
+    // ambos mapas deben tener el mismo tamaño
+    const imgWidth  = widthSegments;
+    const imgHeight = heightSegments;
+
+    canvas.width  = imgWidth;
+    canvas.height = imgHeight;
+
+    ctx.drawImage(treesForbiddenMapImage, 0, 0, imgWidth, imgHeight);
+    treesForbiddenMap = ctx.getImageData(0, 0, imgWidth, imgHeight);
+    treesForbiddenMapData = treesForbiddenMap.data;
+
+    ctx.drawImage(elevationMapImage, 0, 0, imgWidth, imgHeight);
+    elevationMap = ctx.getImageData(0, 0, imgWidth, imgHeight);
+    elevationMapData = elevationMap.data
+
+    console.log("All maps data loaded succesfully");
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+
+function main() {
+    loadMapsData();
+    // buildScene();
+}
+
+loadTextures(main);
diff --git a/tp/src/train.js b/tp/src/train.js
@@ -0,0 +1,311 @@
+import * as THREE from 'three';
+import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
+
+const steamChamberLen = 20;
+const steamChamberRad = 5;
+const steamChamberEndRad = steamChamberRad+0.75;
+const steamChamberEndLen = 5;
+const cabinLen = 10;
+const cabinHeight = 11;
+const cabinRoofHeight = 8;
+const cabinWallThickness = 0.75;
+const wheelRad = 2.75;
+const chassisHeight = 5;
+const wheelThickness = 0.85;
+const chassisOffset = 2.49;
+const wheelOffset = -0.70;
+const steamCylindersLen = 8;
+const crankLen = 22;
+const crankOffset = 3.750;
+const crankWidth = 0.5;
+
+let crankLeft, crankRight;
+
+function buildCabinRoof() {
+    console.log('Building train cabin roof');
+    const geometry = new THREE.BoxGeometry(12, cabinWallThickness, 12);
+    return geometry;
+}
+
+function buildCabin() {
+    console.log('Building train cabin');
+
+    let cabin = [];
+
+    const cabinFront = new THREE.BoxGeometry(
+        steamChamberRad*2,
+        cabinWallThickness,
+        cabinHeight);
+
+    cabinFront.translate(0, cabinLen/2, -cabinHeight/2);
+    cabin.push(cabinFront);
+
+    const cabinLeft = new THREE.BoxGeometry(
+        steamChamberRad*2,
+        cabinWallThickness,
+        cabinHeight);
+
+    cabinLeft.rotateZ(Math.PI/2);
+    cabinLeft.translate(
+        steamChamberRad-cabinWallThickness/2,
+        cabinWallThickness/2,
+        -cabinHeight/2);
+
+    cabin.push(cabinLeft);
+
+    const cabinRight = new THREE.BoxGeometry(
+        steamChamberRad*2,
+        cabinWallThickness,
+        cabinHeight);
+
+    cabinRight.rotateZ(Math.PI/2);
+    cabinRight.translate(
+        -steamChamberRad+cabinWallThickness/2,
+        cabinWallThickness/2,
+        -cabinHeight/2);
+
+    cabin.push(cabinRight);
+
+    const g1 = new THREE.BoxGeometry(
+        cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+
+    g1.rotateZ(Math.PI/2);
+    g1.translate(
+        -steamChamberRad+(cabinWallThickness/2),
+        -steamChamberRad+cabinWallThickness,
+        -cabinHeight-cabinRoofHeight/2);
+
+    cabin.push(g1);
+
+    const g2 = new THREE.BoxGeometry(
+        cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+
+    g2.rotateZ(Math.PI/2);
+    g2.translate(
+        steamChamberRad-cabinWallThickness/2,
+        steamChamberRad,
+        -cabinHeight-cabinRoofHeight/2);
+
+    cabin.push(g2);
+
+    const g3 = new THREE.BoxGeometry(
+        cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+
+    g3.rotateZ(Math.PI/2);
+    g3.translate(
+        steamChamberRad-cabinWallThickness/2,
+        -steamChamberRad+cabinWallThickness,
+        -cabinHeight-cabinRoofHeight/2);
+
+    cabin.push(g3);
+
+    const g4 = new THREE.BoxGeometry(
+        cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+
+    g4.rotateZ(Math.PI/2);
+    g4.translate(
+        -steamChamberRad+cabinWallThickness/2,
+        steamChamberRad,
+        -cabinHeight-cabinRoofHeight/2);
+
+    cabin.push(g4);
+
+    const geometry = BufferGeometryUtils.mergeGeometries(cabin);
+    geometry.rotateX(Math.PI/2);
+    return geometry;
+}
+
+function buildChamber() {
+    let geometries = [];
+
+    const steamChamber = new THREE.CylinderGeometry(steamChamberRad,
+        steamChamberRad, steamChamberLen, 32);
+    
+    geometries.push(steamChamber);
+
+    const steamChamberEnd = new THREE.CylinderGeometry(steamChamberEndRad,
+        steamChamberEndRad, steamChamberEndLen, 32);
+
+    steamChamberEnd.translate(0,steamChamberLen/2 + steamChamberEndLen/2,0);
+    geometries.push(steamChamberEnd);
+
+    const floor = new THREE.BoxGeometry(
+        steamChamberRad*2, steamChamberLen + steamChamberEndLen + cabinLen, 1.0);
+    floor.translate(0, -steamChamberEndLen/2, steamChamberRad);
+    geometries.push(floor);
+
+    const chamberPipeLen = 8;
+    const chamberPipe = new THREE.CylinderGeometry(0.75, 0.75, chamberPipeLen, 32);
+
+    chamberPipe.translate(0,
+        -(steamChamberRad + chamberPipeLen/2)+1.0,
+        -(steamChamberLen+steamChamberEndLen)/2);
+
+    chamberPipe.rotateX(Math.PI/2);
+    geometries.push(chamberPipe);
+
+    const geometry = BufferGeometryUtils.mergeGeometries(geometries);
+    geometry.rotateX(Math.PI/2);
+    geometry.translate(0, steamChamberRad, 0);
+    return geometry;
+}
+
+function buildTrainWheel() {
+    const wheel = new THREE.CylinderGeometry(wheelRad, wheelRad, wheelThickness);
+    wheel.rotateZ(Math.PI/2);
+
+    const wheelBolt = new THREE.CylinderGeometry(wheelRad, wheelRad, wheelThickness);
+    wheelBolt.rotateZ(Math.PI/2);
+
+    const wheelsMaterial = new THREE.MeshPhongMaterial({
+        color: 0x393939, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    return new THREE.Mesh(wheel, wheelsMaterial)
+}
+
+function buildTrainAxe(material) {
+    const axeGeometry = new THREE.CylinderGeometry(0.65, 0.65, 10);
+    axeGeometry.rotateZ(Math.PI/2);
+
+    const axeMaterial = new THREE.MeshPhongMaterial({
+        color: 0x7A7F80, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    return new THREE.Mesh(axeGeometry, axeMaterial);
+}
+
+function buildTrainChassis() {
+    const chassis = new THREE.BoxGeometry(7, 5, steamChamberLen+steamChamberEndLen+cabinLen);
+    return chassis;
+}
+
+export function buildTrain() {
+    console.log('Building train');
+    const train = new THREE.Object3D();
+
+    const chassisGeometry = buildTrainChassis();
+    const chassisMaterial = new THREE.MeshPhongMaterial({
+        color: 0x7A7F80, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    const chassis = new THREE.Mesh(chassisGeometry, chassisMaterial);
+    train.add(chassis);
+
+    const chamberGeometry = buildChamber();
+    const chamberMaterial = new THREE.MeshPhongMaterial({
+        color: 0xFA1A09, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    const chamber = new THREE.Mesh(chamberGeometry, chamberMaterial);
+    chassis.add(chamber);
+    chamber.position.set(0, (chassisHeight + cabinWallThickness)/2, chassisOffset);
+
+    const cabinGeometry = buildCabin();
+    const cabin = new THREE.Mesh(cabinGeometry, chamberMaterial);
+    chassis.add(cabin);
+    cabin.position.set(0,
+        (chassisHeight + cabinWallThickness)/2,
+        -steamChamberLen+(cabinLen/2)+chassisOffset);
+
+    const cabinRoofGeometry = buildCabinRoof();
+    const roofMaterial = new THREE.MeshPhongMaterial({
+        color: 0xFBEC50, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    const cabinRoof = new THREE.Mesh(cabinRoofGeometry, roofMaterial);
+    cabin.add(cabinRoof);
+    cabinRoof.position.set(0, cabinHeight+cabinRoofHeight+cabinWallThickness/2, 0);
+
+    const a1 = buildTrainAxe();
+    chassis.add(a1);
+
+    const a2 = buildTrainAxe();
+    chassis.add(a2);
+
+    const a3 = buildTrainAxe(chassisMaterial);
+    chassis.add(a3);
+
+    a1.position.set(0, wheelOffset, 0);
+    a2.position.set(0, wheelOffset, wheelRad*2.5);
+    a3.position.set(0, wheelOffset, -wheelRad*2.5);
+
+    const cylinderLeft = new THREE.CylinderGeometry(2.25, 2.5, steamCylindersLen);
+    cylinderLeft.rotateX(Math.PI/2);
+    cylinderLeft.translate(steamChamberRad-0.25, 0, steamChamberLen-steamCylindersLen/1.5);
+
+    const cylinderRight = new THREE.CylinderGeometry(2.25, 2.5, steamCylindersLen);
+    cylinderRight.rotateX(Math.PI/2);
+    cylinderRight.translate(-steamChamberRad+0.25, 0, steamChamberLen-steamCylindersLen/1.5);
+
+    const cylindersGeometry = BufferGeometryUtils.mergeGeometries([cylinderRight, cylinderLeft]);
+    const cylindersMaterial = new THREE.MeshPhongMaterial({
+        color: 0x393939, 
+        side: THREE.DoubleSide,
+        shininess: 100.0
+    });
+
+    chassis.add(new THREE.Mesh(cylindersGeometry, cylindersMaterial));
+    chassis.position.set(0,-2,-2.75);
+
+    const w1 = buildTrainWheel();
+    w1.position.set(steamChamberRad-wheelThickness/2.1,0,0);
+    a1.add(w1);
+
+    const w2 = buildTrainWheel();
+    w2.position.set(-steamChamberRad+wheelThickness/2.1,0,0);
+    a1.add(w2);
+
+    const w3 = buildTrainWheel();
+    w3.position.set(steamChamberRad-wheelThickness/2.1,0,0);
+    a2.add(w3);
+
+    const w4 = buildTrainWheel();
+    w4.position.set(-steamChamberRad+wheelThickness/2.1,0,);
+    a2.add(w4);
+
+    const w5 = buildTrainWheel();
+    w5.position.set(steamChamberRad-wheelThickness/2.1,0,0);
+    a3.add(w5);
+
+    const w6 = buildTrainWheel();
+    w6.position.set(-steamChamberRad+wheelThickness/2.1,0,0);
+    a3.add(w6);
+
+    const crankGeometry = new THREE.BoxGeometry(crankWidth, 1.0, crankLen);
+
+    crankRight = new THREE.Mesh(crankGeometry, chassisMaterial);
+    //crankRight.position.set(steamChamberRad, wheelOffset, crankOffset);
+
+    crankLeft = new THREE.Mesh(crankGeometry, chassisMaterial);
+    //crankLeft.position.set(-steamChamberRad, wheelOffset, crankOffset);
+
+    chassis.add(crankLeft);
+    chassis.add(crankRight);
+
+    // chassis.translateY(-wheelOffset);
+    updateTrainCrankPosition();
+
+    train.position.set(0, 1.9, 0);
+    return train;
+}
+
+export function updateTrainCrankPosition(time = 0.0) {
+    crankLeft.position.set(-steamChamberRad-crankWidth/2,
+        wheelOffset+1.00*(Math.sin(time*Math.PI*2)),
+        crankOffset - 1.00*(Math.cos(time*Math.PI*2)));
+
+    crankRight.position.set(steamChamberRad+crankWidth/2,
+        wheelOffset+1.00*(Math.sin(time*Math.PI*2)),
+        crankOffset - 1.00*(Math.cos(time*Math.PI*2)));
+}
diff --git a/tp/src/trees.js b/tp/src/trees.js
@@ -0,0 +1,204 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { vertexShader, fragmentShader } from '/assets/treesShaders.js';
+
+let scene, camera, renderer, container, terrainMaterial, instancedTrees;
+
+const textures = {
+    tierra: { url: '/assets/tierra.jpg', object: null },
+    roca: { url: '/assets/roca.jpg', object: null },
+    pasto: { url: '/assets/pasto.jpg', object: null },
+};
+
+function onResize() {
+    camera.aspect = container.offsetWidth / container.offsetHeight;
+    camera.updateProjectionMatrix();
+    renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+    scene = new THREE.Scene();
+    container = document.getElementById('mainContainer');
+
+    renderer = new THREE.WebGLRenderer();
+    renderer.setClearColor(0x606060);
+    container.appendChild(renderer.domElement);
+
+    camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000);
+    camera.position.set(-50, 60, 50);
+    camera.lookAt(0, 0, 0);
+
+    const controls = new OrbitControls(camera, renderer.domElement);
+
+    const ambientLight = new THREE.AmbientLight(0xffffff);
+    scene.add(ambientLight);
+
+    const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+    scene.add(hemisphereLight);
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+    directionalLight.position.set(100, 100, 100);
+    scene.add(directionalLight);
+
+    const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
+    // scene.add(directionalLightHelper);
+
+    //const gridHelper = new THREE.GridHelper(50, 20);
+    //scene.add(gridHelper);
+
+    const axesHelper = new THREE.AxesHelper(5);
+    scene.add(axesHelper);
+
+    window.addEventListener('resize', onResize);
+    onResize();
+}
+
+function createInstancedTrees(count) {
+    console.log('Generating `' + count + '` instances of tree');
+
+    let logHeight = 4.0;
+
+    const treeLogGeometry    = new THREE.CylinderGeometry(0.30, 0.30, logHeight, 40, 40);
+    const treeLeavesGeometry = new THREE.SphereGeometry(1.75,40,40);
+
+    treeLogGeometry.translate(0, logHeight/2.0, 0);
+
+    const instancedTreeLogGeometry = new THREE.InstancedBufferGeometry();
+    instancedTreeLogGeometry.copy(treeLogGeometry);
+
+    const instancedTreeLeavesGeometry = new THREE.InstancedBufferGeometry();
+    instancedTreeLeavesGeometry.copy(treeLeavesGeometry);
+
+    const treeLogMaterial   = new THREE.MeshPhongMaterial({color: 0x7c3f00});
+    const instancedTreeLogs = new THREE.InstancedMesh(instancedTreeLogGeometry, treeLogMaterial, count);
+
+    const treeLeavesMaterial  = new THREE.MeshPhongMaterial({color: 0x365829});
+    const instancedTreeLeaves = new THREE.InstancedMesh(instancedTreeLeavesGeometry, treeLeavesMaterial, count);
+
+    const rotMatrix = new THREE.Matrix4();
+
+    const translationMatrix = new THREE.Matrix4();
+    const treeLogMatrix    = new THREE.Matrix4();
+    const treeLeavesMatrix = new THREE.Matrix4();
+
+    //let origin = new THREE.Vector3();
+    const RANGE = 50 - 4/2;
+
+    for (let i = 0; i < count; i++) {
+        let position = new THREE.Vector3(
+            (Math.random() - 0.5) * RANGE,
+            0,
+            (Math.random() - 0.5) * RANGE
+        );
+
+        translationMatrix.makeTranslation(position);
+
+        //rotMatrix.lookAt(0, 0, new THREE.Vector3(0, 1, 0));
+        treeLogMatrix.identity();
+        treeLeavesMatrix.identity();
+
+        let scale = 0.5 + (Math.random()*(logHeight/3));
+        treeLogMatrix.makeScale(1, scale, 1);
+        //matrix.premultiply(rotMatrix);
+
+        treeLogMatrix.premultiply(translationMatrix);
+
+        position.y = scale*logHeight;
+        translationMatrix.makeTranslation(position);
+        treeLeavesMatrix.premultiply(translationMatrix);
+
+        instancedTreeLogs.setMatrixAt(i, treeLogMatrix);
+        instancedTreeLeaves.setMatrixAt(i, treeLeavesMatrix);
+    }
+
+    scene.add(instancedTreeLogs);
+    scene.add(instancedTreeLeaves);
+}
+
+function buildScene() {
+    console.log('Building scene');
+
+    console.log('Generating terrain');
+    const terrainGeometry = new THREE.PlaneGeometry(50, 50);
+    //const terrainMaterial = new THREE.MeshPhongMaterial( {color: 0x365829, side: THREE.DoubleSide} );
+    terrainMaterial = new THREE.RawShaderMaterial({
+        uniforms: {
+            tierraSampler: { type: 't', value: textures.tierra.object },
+            rocaSampler: { type: 't', value: textures.roca.object },
+            pastoSampler: { type: 't', value: textures.pasto.object },
+            scale1: { type: 'f', value: 2.0 },
+
+            mask1low: { type: 'f', value: -0.38 },
+            mask1high: { type: 'f', value: 0.1 },
+
+            mask2low: { type: 'f', value: 0.05 },
+            mask2high: { type: 'f', value: -0.70 },
+        },
+        vertexShader: vertexShader,
+        fragmentShader: fragmentShader,
+        side: THREE.DoubleSide,
+    });
+    terrainMaterial.needsUpdate = true;
+
+    const terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
+    terrain.rotateX(Math.PI/2);
+    terrain.position.set(0, 0, 0);
+    scene.add(terrain);
+
+    console.log('Generating trees');
+    createInstancedTrees(35);
+}
+
+function onTextureLoaded(key, texture) {
+    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+    textures[key].object = texture;
+    console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+    const loadingManager = new THREE.LoadingManager();
+
+    loadingManager.onLoad = () => {
+        console.log('All textures loaded');
+        callback();
+    };
+
+    for (const key in textures) {
+        console.log("Loading textures");
+        const loader = new THREE.TextureLoader(loadingManager);
+        const texture = textures[key];
+        texture.object = loader.load(
+            texture.url,
+            onTextureLoaded.bind(this, key),
+            null,
+            (error) => {
+                console.error(error);
+            }
+        );
+    }
+}
+
+
+function createMenu() {
+    const gui = new dat.GUI({ width: 400 });
+    gui.add(terrainMaterial.uniforms.scale1, 'value', 0, 10).name('Texture scale');
+    gui.add(terrainMaterial.uniforms.mask1low, 'value', -1, 1).name('Mask1 Low');
+    gui.add(terrainMaterial.uniforms.mask1high, 'value', -1, 1).name('Mask1 High');
+    gui.add(terrainMaterial.uniforms.mask2low, 'value', -1, 1).name('Mask2 Low');
+    gui.add(terrainMaterial.uniforms.mask2high, 'value', -1, 1).name('Mask2 High');
+}
+
+function mainLoop() {
+    requestAnimationFrame(mainLoop);
+    renderer.render(scene, camera);
+}
+
+function main() {
+    buildScene();
+    createMenu();
+    mainLoop();
+}
+
+setupThreeJs();
+loadTextures(main);
diff --git a/tp/src/tunnel.js b/tp/src/tunnel.js
@@ -0,0 +1,46 @@
+import * as THREE from 'three';
+
+export function generateTunnelGeometry(
+    tunnelHeight = 20, tunnelWidth = 14,
+    tunnelWallThickness = 0.5, tunnelLen = 26) {
+
+    const path = new THREE.Path();
+    path.moveTo(-tunnelWidth/2, 0);
+    path.lineTo(-tunnelWidth/2, tunnelHeight*1/3);
+    path.moveTo(-tunnelWidth/2, tunnelHeight*1/3);
+    path.quadraticCurveTo(0, tunnelHeight, tunnelWidth/2, tunnelHeight*1/3);
+    path.moveTo(tunnelWidth/2, 0);
+    path.lineTo(tunnelWidth/2, 0);
+
+
+    // cerramos la curva con otra de la misma forma con una diferencia de
+    // `tunnelWallThickness`
+    path.lineTo(tunnelWidth/2-tunnelWallThickness, 0);
+    path.moveTo(tunnelWidth/2-tunnelWallThickness, 0);
+
+    path.lineTo(tunnelWidth/2-tunnelWallThickness, tunnelHeight*1/3);
+    path.moveTo(tunnelWidth/2-tunnelWallThickness, tunnelHeight*1/3);
+
+    path.quadraticCurveTo(
+        0, tunnelHeight-(tunnelWallThickness*2),
+        -tunnelWidth/2+tunnelWallThickness, tunnelHeight*1/3);
+
+    path.lineTo(-tunnelWidth/2+tunnelWallThickness, 0);
+    path.moveTo(-tunnelWidth/2+tunnelWallThickness, 0);
+
+    path.lineTo(-tunnelWidth/2, 0);
+    path.moveTo(-tunnelWidth/2, 0);
+
+    const points = path.getPoints();
+    const shape = new THREE.Shape(points);
+
+    const extrudeSettings = {
+        curveSegments: 24,
+        steps: 50,
+        depth: tunnelLen,
+    };
+
+    const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
+    geometry.translate(0, 0, -tunnelLen/2);
+    return geometry;
+}
diff --git a/tp/terrain.html b/tp/terrain.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>Trabajo Practico Sistemas Graficos | Martin Klöckner</title>
+        <link rel="icon" href="data:,"> <!-- Do not request favicon -->
+        <style>
+            html, body, #mainContainer {
+                padding: 0;
+                margin: 0;
+                height: 100%;
+        </style>
+    </head>
+    <body>
+        <div id="mainContainer"></div>
+        <script type="module" src="/src/standalone/terrain.js"></script>
+    </body>
+</html>
diff --git a/tp/track-map.html b/tp/track-map.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>Trabajo Practico Sistemas Graficos | Martin Klöckner</title>
+        <link rel="icon" href="data:,"> <!-- Do not request favicon -->
+        <style>
+            html, body, #mainContainer {
+                padding: 0;
+                margin: 0;
+                height: 100%;
+        </style>
+    </head>
+    <body>
+        <div id="mainContainer"></div>
+        <script type="module" src="/src/standalone/track-map.js"></script>
+    </body>
+</html>
diff --git a/tp/train-tree.gv b/tp/train-tree.gv
@@ -0,0 +1,21 @@
+digraph G {
+    node [ shape=box ];
+    rankdir=TD;
+    compound=true;
+    
+    subgraph cluster {
+        label="Train";
+        style="dashed";
+        A [ label="Chassis" ];
+        B [ label="Chamber" ];
+        C [ label="Cabin" ];
+        D [ label="Axes" ]
+        E [ label="Wheels" ]
+        F [ label="Cylinders" ]
+        G [ label="Floor" ]
+        H [ label="Cranks" ]
+    }
+
+    A -> {B, C, D, F, G, H};
+    D -> E;
+}
diff --git a/tp/train-tree.png b/tp/train-tree.png
Binary files differ.
diff --git a/tp/train.html b/tp/train.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>Trabajo Practico Sistemas Graficos | Martin Klöckner</title>
+        <link rel="icon" href="data:,"> <!-- Do not request favicon -->
+        <style>
+            html, body, #mainContainer {
+                padding: 0;
+                margin: 0;
+                height: 100%;
+        </style>
+    </head>
+    <body>
+        <div id="mainContainer"></div>
+        <script type="module" src="/src/standalone/train.js"></script>
+    </body>
+</html>
diff --git a/tp/trees.html b/tp/trees.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>Trabajo Practico Sistemas Graficos | Martin Klöckner</title>
+        <link rel="icon" href="data:,"> <!-- Do not request favicon -->
+        <style>
+            html, body, #mainContainer {
+                padding: 0;
+                margin: 0;
+                height: 100%;
+        </style>
+    </head>
+    <body>
+        <div id="mainContainer"></div>
+        <script type="module" src="/src/standalone/trees.js"></script>
+    </body>
+</html>
diff --git a/tp/tunnel.html b/tp/tunnel.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>Trabajo Practico Sistemas Graficos | Martin Klöckner</title>
+        <link rel="icon" href="data:,"> <!-- Do not request favicon -->
+        <style>
+            html, body, #mainContainer {
+                padding: 0;
+                margin: 0;
+                height: 100%;
+        </style>
+    </head>
+    <body>
+        <div id="mainContainer"></div>
+        <script type="module" src="/src/standalone/tunnel.js"></script>
+    </body>
+</html>