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:
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>