TA159

Notas, resueltos y trabajos practicos de la materia Sistemas Gráficos
Index Commits Files Refs Submodules README LICENSE
commit 1287c5fbc3f17fc02584765d9afa29915c48a348
parent b9c27482ccf1444b51ce5f6d3ee0d4d354da5afe
Author: Martin Kloeckner <mjkloeckner@gmail.com>
Date:   Thu, 27 Jun 2024 19:41:43 -0300

move all standalone scene elements to new folder `/src/standalone`

Diffstat:
Mtp/bridge.html | 2+-
Mtp/rails.html | 2+-
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+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtp/terrain.html | 2+-
Mtp/track-map.html | 2+-
Mtp/train.html | 2+-
Mtp/trees.html | 2+-
Mtp/tunnel.html | 2+-
14 files changed, 2125 insertions(+), 7 deletions(-)
diff --git a/tp/bridge.html b/tp/bridge.html
@@ -13,6 +13,6 @@
     </head>
     <body>
         <div id="mainContainer"></div>
-        <script type="module" src="/src/bridge.js"></script>
+        <script type="module" src="/src/standalone/bridge.js"></script>
     </body>
 </html>
diff --git a/tp/rails.html b/tp/rails.html
@@ -13,6 +13,6 @@
     </head>
     <body>
         <div id="mainContainer"></div>
-        <script type="module" src="/src/rails.js"></script>
+        <script type="module" src="/src/standalone/rails.js"></script>
     </body>
 </html>
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/terrain.html b/tp/terrain.html
@@ -13,6 +13,6 @@
     </head>
     <body>
         <div id="mainContainer"></div>
-        <script type="module" src="/src/terrain.js"></script>
+        <script type="module" src="/src/standalone/terrain.js"></script>
     </body>
 </html>
diff --git a/tp/track-map.html b/tp/track-map.html
@@ -13,6 +13,6 @@
     </head>
     <body>
         <div id="mainContainer"></div>
-        <script type="module" src="/src/track-map.js"></script>
+        <script type="module" src="/src/standalone/track-map.js"></script>
     </body>
 </html>
diff --git a/tp/train.html b/tp/train.html
@@ -13,6 +13,6 @@
     </head>
     <body>
         <div id="mainContainer"></div>
-        <script type="module" src="/src/train.js"></script>
+        <script type="module" src="/src/standalone/train.js"></script>
     </body>
 </html>
diff --git a/tp/trees.html b/tp/trees.html
@@ -13,6 +13,6 @@
     </head>
     <body>
         <div id="mainContainer"></div>
-        <script type="module" src="/src/trees.js"></script>
+        <script type="module" src="/src/standalone/trees.js"></script>
     </body>
 </html>
diff --git a/tp/tunnel.html b/tp/tunnel.html
@@ -13,6 +13,6 @@
     </head>
     <body>
         <div id="mainContainer"></div>
-        <script type="module" src="/src/tunnel.js"></script>
+        <script type="module" src="/src/standalone/tunnel.js"></script>
     </body>
 </html>