commit 1287c5fbc3f17fc02584765d9afa29915c48a348
parent b9c27482ccf1444b51ce5f6d3ee0d4d354da5afe
Author: Martin Kloeckner <mjkloeckner@gmail.com>
Date: Thu, 27 Jun 2024 19:41:43 -0300
move all standalone scene elements to new folder `/src/standalone`
Diffstat:
14 files changed, 2125 insertions(+), 7 deletions(-)
diff --git a/tp/bridge.html b/tp/bridge.html
@@ -13,6 +13,6 @@
</head>
<body>
<div id="mainContainer"></div>
- <script type="module" src="/src/bridge.js"></script>
+ <script type="module" src="/src/standalone/bridge.js"></script>
</body>
</html>
diff --git a/tp/rails.html b/tp/rails.html
@@ -13,6 +13,6 @@
</head>
<body>
<div id="mainContainer"></div>
- <script type="module" src="/src/rails.js"></script>
+ <script type="module" src="/src/standalone/rails.js"></script>
</body>
</html>
diff --git a/tp/src/standalone/bridge.js b/tp/src/standalone/bridge.js
@@ -0,0 +1,342 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
+
+let scene, camera, renderer, container;
+
+const textures = {
+ tierra: { url: '/assets/tierraSeca.jpg', object: null },
+ ladrillos: { url: '/assets/pared-de-ladrillos.jpg', object: null },
+};
+
+function onResize() {
+ camera.aspect = container.offsetWidth / container.offsetHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+ scene = new THREE.Scene();
+ container = document.getElementById('mainContainer');
+
+ renderer = new THREE.WebGLRenderer();
+ renderer.setClearColor(0x606060);
+ // renderer.setClearColor(0xFFFFFF);
+ container.appendChild(renderer.domElement);
+
+ camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000);
+ camera.position.set(25, 25, 25);
+ camera.lookAt(10, 10, 10);
+
+ const controls = new OrbitControls(camera, renderer.domElement);
+
+ const ambientLight = new THREE.AmbientLight(0xaaaaaa);
+ scene.add(ambientLight);
+
+ const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+ scene.add(hemisphereLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(100, 100, 100);
+ scene.add(directionalLight);
+
+ const gridHelper = new THREE.GridHelper(50, 20);
+ scene.add(gridHelper);
+
+ const axesHelper = new THREE.AxesHelper(5);
+ scene.add(axesHelper);
+
+ window.addEventListener('resize', onResize);
+ onResize();
+}
+
+function onTextureLoaded(key, texture) {
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+ textures[key].object = texture;
+ console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+ const loadingManager = new THREE.LoadingManager();
+
+ loadingManager.onLoad = () => {
+ console.log('All textures loaded');
+ callback();
+ };
+
+ for (const key in textures) {
+ console.log("Loading textures");
+ const loader = new THREE.TextureLoader(loadingManager);
+ const texture = textures[key];
+ texture.object = loader.load(
+ texture.url,
+ onTextureLoaded.bind(this, key),
+ null,
+ (error) => {
+ console.error(error);
+ }
+ );
+ }
+}
+
+const arcWidth = 5;
+const arcCount = 4;
+const arcRadius = arcWidth/2;
+const columnHeight = 5;
+const columnWidth = 1.50;
+const topPadding = 0.50;
+const startPadding = 10;
+const endPadding = startPadding;
+const bridgeWallThickness = 2.5;
+const bridgeLen = arcCount*(columnWidth+arcWidth)+columnWidth+startPadding+endPadding;
+const bridgeHeight = columnHeight+arcRadius+topPadding;
+
+function generateBridgeWall() {
+ const path = new THREE.Path();
+
+ // generate the arcs
+ for(let i = 1; i <= arcCount; ++i) {
+ path.lineTo(startPadding+i*columnWidth+((i-1)*arcWidth), 0);
+ path.moveTo(startPadding+i*columnWidth+((i-1)*arcWidth), 0);
+ path.lineTo(startPadding+i*columnWidth+((i-1)*arcWidth), columnHeight);
+ path.arc(arcRadius, 0, arcRadius, Math.PI, 0, true)
+ path.moveTo(startPadding+i*(columnWidth+arcWidth), 0);
+ path.lineTo(startPadding+i*(columnWidth+arcWidth), 0);
+ }
+
+ // no we close the curve
+ path.lineTo(bridgeLen, 0);
+ path.lineTo(bridgeLen, bridgeHeight);
+
+ path.lineTo(0, bridgeHeight);
+ path.lineTo(0, 0);
+
+ /*
+ // muestra la curva utilizada para la extrusión
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
+ const lineMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
+ const curveObject = new THREE.Line(geometry, lineMaterial);
+ scene.add(curveObject);
+ */
+
+ const points = path.getPoints();
+ const shape = new THREE.Shape(points);
+
+ const extrudeSettings = {
+ curveSegments: 24,
+ steps: 50,
+ depth: bridgeWallThickness,
+ bevelEnabled: false
+ };
+
+ const bridgeWallGeometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
+ bridgeWallGeometry.translate(-bridgeLen/2, 0, -bridgeWallThickness/2);
+ return bridgeWallGeometry;
+}
+
+const squareTubeRadius = 0.15;
+function generateBridgeCage(squaresCount = 3) {
+ const squaresSideLen = 10;
+ const bridgeCageLen = squaresCount * squaresSideLen;
+
+ let geometries = []
+
+ let cylinderBase, cylinderCorner, cylinderCrossbar;
+ for(let square = 0; square < squaresCount; ++square) {
+ // 0 -> 00
+ // 1 -> 01
+ // 2 -> 10
+ // 3 -> 11
+ for(let i = 0; i < 4; ++i) {
+ cylinderBase = new THREE.CylinderGeometry(
+ squareTubeRadius, squareTubeRadius, squaresSideLen);
+
+ cylinderCorner = cylinderBase.clone();
+
+ const squareHypotenuse = Math.sqrt(2*squaresSideLen*squaresSideLen);
+ cylinderCrossbar = new THREE.CylinderGeometry(
+ squareTubeRadius, squareTubeRadius, squareHypotenuse);
+
+ if((i % 2) == 0) {
+ cylinderBase.rotateZ(Math.PI/2);
+ cylinderBase.translate(
+ 0,
+ square*(squaresSideLen),
+ ((-1)**(i>>1))*squaresSideLen/2);
+
+ cylinderCrossbar.rotateZ((-1)**((i>>1))*Math.PI/4);
+ cylinderCrossbar.translate(
+ 0,
+ square*(squaresSideLen)+(squaresSideLen/2),
+ ((-1)**(i>>1))*squaresSideLen/2);
+
+ cylinderCorner.translate(
+ ((-1)**(i>>1))*squaresSideLen/2,
+ square*(squaresSideLen)+(squaresSideLen/2),
+ ((-1)**(i&1))*squaresSideLen/2);
+ } else {
+ cylinderBase.rotateX(Math.PI/2);
+ cylinderBase.translate(
+ ((-1)**(i>>1))*squaresSideLen/2,
+ square*(squaresSideLen),
+ 0);
+
+ cylinderCrossbar.rotateX((-1)**((i>>1))*Math.PI/4);
+ cylinderCrossbar.translate(
+ ((-1)**(i>>1))*squaresSideLen/2,
+ square*(squaresSideLen)+(squaresSideLen/2),
+ 0);
+
+ cylinderCorner.translate(
+ ((-1)**(i>>1))*squaresSideLen/2,
+ square*(squaresSideLen)+(squaresSideLen/2),
+ ((-1)**(i&1))*squaresSideLen/2);
+ }
+ geometries.push(cylinderBase);
+ geometries.push(cylinderCrossbar);
+ geometries.push(cylinderCorner);
+ }
+
+ // agregamos un cuadrado mas para 'cerrar' la 'jaula'
+ if((square + 1) == squaresCount) {
+ for(let i = 0; i < 4; ++i) {
+ cylinderBase = new THREE.CylinderGeometry(
+ squareTubeRadius, squareTubeRadius, squaresSideLen);
+
+ if((i % 2) == 0) {
+ cylinderBase.rotateZ(Math.PI/2);
+ cylinderBase.translate(
+ 0,
+ (square+1)*(squaresSideLen),
+ ((-1)**(i>>1))*squaresSideLen/2);
+ } else {
+ cylinderBase.rotateX(Math.PI/2);
+ cylinderBase.translate(
+ ((-1)**(i>>1))*squaresSideLen/2,
+ (square+1)*(squaresSideLen), 0);
+ }
+ geometries.push(cylinderBase);
+ }
+ }
+ }
+
+ const bridgeCage = mergeGeometries(geometries);
+ bridgeCage.rotateZ(Math.PI/2);
+ bridgeCage.translate(bridgeCageLen/2, squaresSideLen/2, 0);
+ return bridgeCage;
+}
+
+function generateBridge() {
+ const bridgeWidth = 10;
+ const roadwayHeight = 2;
+
+ const leftWallGeometry = generateBridgeWall();
+ leftWallGeometry.translate(0, 0, -bridgeWidth/2);
+
+ const rightWallGeometry = generateBridgeWall();
+ rightWallGeometry.translate(0, 0, bridgeWidth/2)
+
+ const bridgeColumnsGeometry = mergeGeometries([leftWallGeometry, rightWallGeometry]);
+ const bridgeRoadwayGeometry = new THREE.BoxGeometry(
+ bridgeLen, roadwayHeight, bridgeWidth+bridgeWallThickness,
+ );
+
+ bridgeRoadwayGeometry.translate(0, bridgeHeight+roadwayHeight/2, 0);
+
+ textures.ladrillos.object.wrapS = THREE.RepeatWrapping;
+ textures.ladrillos.object.wrapT = THREE.RepeatWrapping;
+ textures.ladrillos.object.repeat.set(0.75*0.15, 0.75*0.35);
+ textures.ladrillos.object.anisotropy = 16;
+
+ const bridgeMaterial = new THREE.MeshPhongMaterial({
+ side: THREE.DoubleSide,
+ transparent: false,
+ opacity: 1.0,
+ shininess: 10,
+ map: textures.ladrillos.object
+ });
+
+ /*
+ textures.ladrillos2.object.wrapS = THREE.RepeatWrapping;
+ textures.ladrillos2.object.wrapT = THREE.RepeatWrapping;
+ textures.ladrillos2.object.repeat.set(0.75*5, 0.75*0.75);
+ textures.ladrillos2.object.anisotropy = 16;
+
+ const roadwayMaterial = new THREE.MeshPhongMaterial({
+ side: THREE.DoubleSide,
+ transparent: false,
+ opacity: 1.0,
+ shininess: 10,
+ map: textures.ladrillos2.object
+ // color: 0xFF0000
+ });
+
+ const bridgeRoadway = new THREE.Mesh(bridgeRoadwayGeometry, roadwayMaterial);
+ scene.add(bridgeRoadway);
+ */
+
+ const bridgeColumns = new THREE.Mesh(bridgeColumnsGeometry, bridgeMaterial);
+ scene.add(bridgeColumns);
+
+ // para reutilizar la textura de ladrillos usada en los arcos se escalan las
+ // coordenadas uv de la geometria de la parte superior
+ let uvs = bridgeRoadwayGeometry.attributes.uv.array;
+ for (let i = 0, len = uvs.length; i < len; i++) {
+ uvs[i] = (i % 2) ? uvs[i]*2.50 : uvs[i]*30.0;
+ }
+
+ const bridgeRoadway = new THREE.Mesh(bridgeRoadwayGeometry, bridgeMaterial);
+ scene.add(bridgeRoadway);
+
+ const cageGeometry = generateBridgeCage()
+ cageGeometry.translate(0, bridgeHeight+roadwayHeight-squareTubeRadius*2, 0);
+
+ const cageMaterial = new THREE.MeshPhongMaterial({
+ side: THREE.DoubleSide,
+ transparent: false,
+ opacity: 1.0,
+ shininess: 10,
+ color: 0xFFFFFF
+ });
+
+ const bridgeCage = new THREE.Mesh(cageGeometry, cageMaterial);
+ scene.add(bridgeCage);
+
+ const roadwayFloorGeometry = new THREE.PlaneGeometry(
+ bridgeWidth+bridgeWallThickness,
+ bridgeLen);
+
+ roadwayFloorGeometry.rotateZ(Math.PI/2)
+ roadwayFloorGeometry.rotateX(Math.PI/2)
+ roadwayFloorGeometry.translate(0, bridgeHeight+roadwayHeight, 0)
+
+ textures.tierra.object.wrapS = THREE.MirroredRepeatWrapping;
+ textures.tierra.object.wrapT = THREE.MirroredRepeatWrapping;
+ textures.tierra.object.repeat.set(1, 5);
+ textures.tierra.object.anisotropy = 16;
+
+ const roadwayFloorMaterial = new THREE.MeshPhongMaterial({
+ side: THREE.DoubleSide,
+ transparent: false,
+ opacity: 1.0,
+ shininess: 10,
+ map: textures.tierra.object
+ });
+
+ const roadwayFloor = new THREE.Mesh(roadwayFloorGeometry, roadwayFloorMaterial);
+ scene.add(roadwayFloor)
+}
+
+function mainLoop() {
+ requestAnimationFrame(mainLoop);
+ renderer.render(scene, camera);
+}
+
+function main() {
+ generateBridge();
+ mainLoop();
+}
+
+setupThreeJs();
+loadTextures(main);
diff --git a/tp/src/standalone/rails.js b/tp/src/standalone/rails.js
@@ -0,0 +1,274 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+
+import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js';
+import { ParametricGeometries } from 'three/examples/jsm/geometries/ParametricGeometries.js';
+import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
+
+let scene, camera, renderer, container, terrainMaterial, instancedTrees;
+let spherePath;
+let railsPath;
+let railsFoundationShape;
+
+const textures = {
+ tierra: { url: '/assets/tierra.jpg', object: null },
+ roca: { url: '/assets/roca.jpg', object: null },
+ pasto: { url: '/assets/pasto.jpg', object: null },
+ durmientes: { url: '/assets/durmientes.jpg', object: null },
+};
+
+function onResize() {
+ camera.aspect = container.offsetWidth / container.offsetHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+ scene = new THREE.Scene();
+ container = document.getElementById('mainContainer');
+
+ renderer = new THREE.WebGLRenderer();
+ renderer.setClearColor(0x606060);
+ container.appendChild(renderer.domElement);
+
+ camera = new THREE.PerspectiveCamera(
+ 35, window.innerWidth / window.innerHeight, 0.1, 1000);
+
+ camera.position.set(-10, 15, -10);
+ camera.lookAt(0, 0, 0);
+
+ const controls = new OrbitControls(camera, renderer.domElement);
+
+ const ambientLight = new THREE.AmbientLight(0xffffff);
+ scene.add(ambientLight);
+
+ const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+ scene.add(hemisphereLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(100, 100, 100);
+ scene.add(directionalLight);
+
+ const gridHelper = new THREE.GridHelper(50, 20);
+ scene.add(gridHelper);
+
+ const axesHelper = new THREE.AxesHelper(5);
+ scene.add(axesHelper);
+
+ window.addEventListener('resize', onResize);
+ onResize();
+}
+
+function onTextureLoaded(key, texture) {
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+ textures[key].object = texture;
+ console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+ const loadingManager = new THREE.LoadingManager();
+
+ loadingManager.onLoad = () => {
+ console.log('All textures loaded');
+ callback();
+ };
+
+ for (const key in textures) {
+ console.log("Loading textures");
+ const loader = new THREE.TextureLoader(loadingManager);
+ const texture = textures[key];
+ texture.object = loader.load(
+ texture.url,
+ onTextureLoaded.bind(this, key),
+ null,
+ (error) => {
+ console.error(error);
+ }
+ );
+ }
+}
+
+function parametricRailsFoundationFunction(u, v, target) {
+ const rotMatrix = new THREE.Matrix4();
+ const translationMatrix = new THREE.Matrix4();
+ const levelMatrix = new THREE.Matrix4();
+
+ let railsPathPos = railsPath.getPointAt(v);
+ let railsFoundationShapePos = railsFoundationShape.getPointAt(u);
+ // TODO: make `railsFoundationShape` smaller and remove this multiplication
+ railsFoundationShapePos.multiplyScalar(0.5);
+
+ let tangente = new THREE.Vector3();
+ let binormal = new THREE.Vector3();
+ let normal = new THREE.Vector3();
+
+ tangente = railsPath.getTangent(v);
+
+ tangente.normalize();
+ binormal = new THREE.Vector3(0, 1, 0);
+ normal.crossVectors(tangente, binormal);
+
+ translationMatrix.makeTranslation(railsPathPos);
+
+ rotMatrix.identity();
+ levelMatrix.identity();
+
+ levelMatrix.makeTranslation(railsPathPos);
+ rotMatrix.makeBasis(normal, tangente, binormal);
+ levelMatrix.multiply(rotMatrix);
+ railsFoundationShapePos.applyMatrix4(levelMatrix);
+
+ const x = railsFoundationShapePos.x;
+ const y = railsFoundationShapePos.y;
+ const z = railsFoundationShapePos.z;
+ target.set(x, y, z);
+}
+
+export function buildRailsFoundation() {
+ railsFoundationShape = new THREE.CatmullRomCurve3([
+ new THREE.Vector3( -2.00, 0.00, 0.00),
+ new THREE.Vector3( -1.00, 0.00, 0.50),
+ new THREE.Vector3( 0.00, 0.00, 0.55),
+ new THREE.Vector3( 1.00, 0.00, 0.50),
+ new THREE.Vector3( 2.00, 0.00, 0.00),
+ ], false);
+
+ /*
+ // show rails foundation shape
+ const points = railsFoundationShape.getPoints(50);
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
+ const lineMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
+ const curveObject = new THREE.Line(geometry, lineMaterial);
+ scene.add(curveObject);
+ */
+ const pGeometry = new ParametricGeometry(
+ parametricRailsFoundationFunction, 100, 100);
+
+ textures.durmientes.object.wrapS = THREE.RepeatWrapping;
+ textures.durmientes.object.wrapT = THREE.RepeatWrapping;
+ textures.durmientes.object.repeat.set(1, 60);
+ textures.durmientes.object.anisotropy = 16;
+
+ /*
+ // load into `map` the example texture
+ const map = new THREE.TextureLoader().load('https://threejs.org/examples/textures/uv_grid_opengl.jpg');
+ map.wrapS = map.wrapT = THREE.RepeatWrapping;
+ map.repeat.set(1, 30);
+ map.anisotropy = 16;
+ // map.rotation = Math.PI/2;
+ */
+
+ const pMaterial = new THREE.MeshPhongMaterial({
+ side: THREE.DoubleSide,
+ transparent: false,
+ opacity: 1.0,
+ shininess: 10,
+ map: textures.durmientes.object
+ });
+ const pMesh = new THREE.Mesh(pGeometry, pMaterial);
+ scene.add(pMesh);
+}
+
+// `position` es de tipo `THREE.Vector3` y representa la translacion de la
+// forma del rail con respecto al origen del sist. de coordenadas de modelado
+function getParametricRailsFunction(radius, position) {
+ return function parametricRails(u, v, target) {
+ const rotMatrix = new THREE.Matrix4();
+ const translationMatrix = new THREE.Matrix4();
+ const levelMatrix = new THREE.Matrix4();
+
+ let railsShape = new THREE.Vector3();
+
+ let railsPathPos = railsPath.getPointAt(v);
+ let railsShapePos = new THREE.Vector3(
+ Math.cos(u*6.28) + position.x,
+ position.y,
+ Math.sin(u*6.28) + position.z);
+
+ railsShapePos.multiplyScalar(0.1*railsRadius);
+
+ let tangente = new THREE.Vector3();
+ let binormal = new THREE.Vector3();
+ let normal = new THREE.Vector3();
+
+ // https://threejs.org/docs/index.html?q=curve#api/en/extras/core/Curve.getTangent
+ tangente = railsPath.getTangentAt(v);
+ binormal = new THREE.Vector3(0, 1, 0);
+ normal.crossVectors(tangente, binormal);
+
+ translationMatrix.makeTranslation(railsPathPos);
+
+ rotMatrix.identity();
+ levelMatrix.identity();
+
+ levelMatrix.makeTranslation(railsPathPos);
+ rotMatrix.makeBasis(normal, tangente, binormal);
+ levelMatrix.multiply(rotMatrix);
+ railsShapePos.applyMatrix4(levelMatrix);
+
+ const x = railsShapePos.x;
+ const y = railsShapePos.y;
+ const z = railsShapePos.z;
+ target.set(x, y, z);
+ }
+}
+
+const railsRadius = 0.35;
+function buildRails() {
+ let railsGeometries = [];
+
+ const leftRailGeometryFunction = getParametricRailsFunction(railsRadius,
+ new THREE.Vector3( 6, 0, railsRadius+8));
+
+ const rightRailGeometryFunction = getParametricRailsFunction(railsRadius,
+ new THREE.Vector3(-6, 0, railsRadius+8));
+
+ const leftRailGeometry = new ParametricGeometry(leftRailGeometryFunction, 100, 500);
+ const rightRailGeometry = new ParametricGeometry(rightRailGeometryFunction, 100, 500);
+
+ railsGeometries.push(leftRailGeometry);
+ railsGeometries.push(rightRailGeometry);
+
+ const railsMaterial = new THREE.MeshPhongMaterial({
+ side: THREE.DoubleSide,
+ transparent: false,
+ opacity: 1.0,
+ shininess: 10,
+ color: 0xFFFFFF
+ });
+
+ const railsGeometry = mergeGeometries(railsGeometries);
+ const rails = new THREE.Mesh(railsGeometry, railsMaterial);
+ scene.add(rails);
+}
+
+function mainLoop() {
+ requestAnimationFrame(mainLoop);
+ renderer.render(scene, camera);
+}
+
+function main() {
+ railsPath = new THREE.CatmullRomCurve3([
+ new THREE.Vector3(-10, 0, 10),
+ new THREE.Vector3( 10, 0, 10),
+ new THREE.Vector3( 10, 0, -10),
+ new THREE.Vector3(-10, 0, -10),
+ ], true);
+
+ /*
+ // muestra la curva utilizada para el camino de `rails`
+ const railsPathPoints = railsPath.getPoints(50);
+ const railsPathGeometry = new THREE.BufferGeometry().setFromPoints(railsPathPoints);
+ const railsPathMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
+ const railsPathMesh = new THREE.Line(railsPathGeometry, railsPathMaterial);
+ scene.add(railsPathMesh);
+ */
+
+ buildRailsFoundation();
+ buildRails();
+ mainLoop();
+}
+
+setupThreeJs();
+loadTextures(main);
diff --git a/tp/src/standalone/terrain.js b/tp/src/standalone/terrain.js
@@ -0,0 +1,371 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { vertexShader, fragmentShader } from '/assets/shaders.js';
+
+let scene, camera, renderer, container, terrainMaterial, terrainGeometry, terrain;
+
+const widthSegments = 100;
+const heightSegments = 100;
+const amplitude = 8;
+const amplitudeBottom = -1.00;
+
+const textures = {
+ tierra: { url: '/assets/tierra.jpg', object: null },
+ roca: { url: '/assets/roca.jpg', object: null },
+ pasto: { url: '/assets/pasto.jpg', object: null },
+ elevationMap: { url: '/assets/elevation_map2.png', object: null },
+};
+
+function onResize() {
+ camera.aspect = container.offsetWidth / container.offsetHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+ scene = new THREE.Scene();
+ container = document.getElementById('mainContainer');
+
+ renderer = new THREE.WebGLRenderer();
+ renderer.setClearColor(0x606060);
+ container.appendChild(renderer.domElement);
+
+ camera = new THREE.PerspectiveCamera(
+ 35, window.innerWidth/window.innerHeight, 0.1, 1000);
+ camera.position.set(100, 120, -100);
+ camera.lookAt(0, 0, 0);
+
+ const controls = new OrbitControls(camera, renderer.domElement);
+
+ const ambientLight = new THREE.AmbientLight(0xffffff);
+ scene.add(ambientLight);
+
+ const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+ //scene.add(hemisphereLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(100, 100, 100);
+ scene.add(directionalLight);
+
+ const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
+ // scene.add(directionalLightHelper);
+
+ const gridHelper = new THREE.GridHelper(150, 150);
+ scene.add(gridHelper);
+
+ const axesHelper = new THREE.AxesHelper( 5 );
+ scene.add( axesHelper );
+
+ window.addEventListener('resize', onResize);
+ onResize();
+}
+
+// obtiene una posicion aleatoria en el terreno, para obtener la altura del
+// terreno utiliza el mapa de elevacion
+function getRandomPositionInTerrain() {
+ let canvas = document.createElement('canvas');
+ let ctx = canvas.getContext('2d');
+ let img = textures.elevationMap.object.image;
+
+ canvas.width = widthSegments;
+ canvas.height = heightSegments;
+
+ ctx.drawImage(img, 0, 0, widthSegments, heightSegments);
+ let imageData = ctx.getImageData(0, 0, widthSegments, heightSegments);
+ let data = imageData.data;
+ const quadsPerRow = widthSegments - 1;
+
+ const x = Math.random();
+ const z = Math.random();
+
+ const elevationMapData = Math.floor((x + z) * widthSegments);
+ const indexX = Math.floor(x * widthSegments);
+ const indexZ = Math.floor(z * heightSegments);
+ const y = data[(indexX + indexZ * widthSegments) * 4] / 255;
+
+ const position = new THREE.Vector3((
+ x - 0.5) * widthSegments,
+ y * amplitude,
+ (z - 0.5) * heightSegments);
+
+ return position;
+}
+
+function createInstancedTrees(count) {
+ console.log('Generating `' + count + '` instances of tree');
+
+ let logHeight = 4.0;
+ const treeLogGeometry = new THREE.CylinderGeometry(
+ 0.30, 0.30, logHeight, 40, 40);
+ treeLogGeometry.translate(0, logHeight/2.0, 0);
+ const instancedTreeLogGeometry = new THREE.InstancedBufferGeometry();
+ instancedTreeLogGeometry.copy(treeLogGeometry);
+ const treeLogMaterial = new THREE.MeshPhongMaterial({color: 0x7c3f00});
+ const instancedTreeLogs = new THREE.InstancedMesh(
+ instancedTreeLogGeometry,
+ treeLogMaterial,
+ count);
+
+ const treeLeavesGeometry = new THREE.SphereGeometry(1.75,40,40);
+ const instancedTreeLeavesGeometry = new THREE.InstancedBufferGeometry();
+ instancedTreeLeavesGeometry.copy(treeLeavesGeometry);
+ const treeLeavesMaterial = new THREE.MeshPhongMaterial({color: 0x365829});
+ const instancedTreeLeaves = new THREE.InstancedMesh(
+ instancedTreeLeavesGeometry,
+ treeLeavesMaterial,
+ count);
+
+ const rotMatrix = new THREE.Matrix4();
+ const translationMatrix = new THREE.Matrix4();
+ const treeLogMatrix = new THREE.Matrix4();
+ const treeLeavesMatrix = new THREE.Matrix4();
+
+ for (let i = 0; i < count; i++) {
+ let position = getRandomPositionInTerrain();
+ let j = 0;
+ while((position.y > 4.0) || (position.y < 2.5)) {
+ position = getRandomPositionInTerrain();
+ // console.log(position);
+ if(j++ == 100) {
+ break;
+ }
+ }
+
+ position.y += amplitudeBottom;
+ translationMatrix.makeTranslation(position);
+ treeLogMatrix.identity();
+ treeLeavesMatrix.identity();
+
+ let scale = 0.5 + (Math.random()*(logHeight/3));
+ treeLogMatrix.makeScale(1, scale, 1);
+ treeLogMatrix.premultiply(translationMatrix);
+
+ position.y += scale * logHeight;
+ translationMatrix.makeTranslation(position);
+ treeLeavesMatrix.premultiply(translationMatrix);
+
+ instancedTreeLogs.setMatrixAt(i, treeLogMatrix);
+ instancedTreeLeaves.setMatrixAt(i, treeLeavesMatrix);
+ }
+
+ return [instancedTreeLogs, instancedTreeLeaves];
+}
+
+// La funcion devuelve una geometria de Three.js
+// width: Ancho del plano
+// height: Alto del plano
+// amplitude: Amplitud de la elevacion
+// widthSegments: Numero de segmentos en el ancho
+// heightSegments: Numero de segmentos en el alto
+// texture: Textura que se usara para la elevacion
+function elevationGeometry(width, height, amplitude, widthSegments, heightSegments, texture) {
+ console.log('Generating terrain geometry');
+ let geometry = new THREE.BufferGeometry();
+
+ const positions = [];
+ const indices = [];
+ const normals = [];
+ const uvs = [];
+
+ // Creamos un canvas para poder leer los valores de los píxeles de la textura
+ let canvas = document.createElement('canvas');
+ let ctx = canvas.getContext('2d');
+ let img = texture.image;
+
+ // Ajustamos el tamaño del canvas segun la cantidad de segmentos horizontales y verticales
+ canvas.width = widthSegments;
+ canvas.height = heightSegments;
+
+ // Dibujamos la textura en el canvas en la escala definida por widthSegments y heightSegments
+ ctx.drawImage(img, 0, 0, widthSegments, heightSegments);
+
+ // Obtenemos los valores de los píxeles de la textura
+ let imageData = ctx.getImageData(0, 0, widthSegments, heightSegments);
+ let data = imageData.data; // Este es un array con los valores de los píxeles
+
+ const quadsPerRow = widthSegments - 1;
+
+ // Recorremos los segmentos horizontales y verticales
+ for (let i = 0; i < widthSegments - 1; i++) {
+ for (let j = 0; j < heightSegments - 1; j++) {
+ // Obtenemos los valores de los píxeles de los puntos adyacentes
+ let xPrev = undefined;
+ let xNext = undefined;
+ let yPrev = undefined;
+ let yNext = undefined;
+
+ // Obtenemos el valor del pixel en la posicion i, j
+ // console.log('getting elevation map value at: (' + i + ',' + j + ')');
+ let z0 = data[(i + j * widthSegments) * 4] / 255;
+
+ // Obtenemos los valores de los píxeles adyacentes
+ xPrev = i > 0 ? data[(i - 1 + j * widthSegments) * 4] / 255 : undefined;
+ xNext = i < widthSegments - 1 ? (xNext = data[(i + 1 + j * widthSegments) * 4] / 255) : undefined;
+
+ yPrev = j > 0 ? data[(i + (j - 1) * widthSegments) * 4] / 255 : undefined;
+ yNext = j < heightSegments - 1 ? data[(i + (j + 1) * widthSegments) * 4] / 255 : undefined;
+
+ // calculamos la diferencia entre los valores de los píxeles adyacentes
+ // en el eje `x` y en el eje `y` de la imagen (en el espacio de la textura
+ // Ojo no confundir con el espacio 3D del modelo 3D donde Y es la altura)
+ let deltaX;
+ if (xPrev == undefined) {
+ deltaX = xNext - z0;
+ } else if (yNext == undefined) {
+ deltaX = xPrev - z0;
+ } else {
+ deltaX = (xNext - xPrev) / 2;
+ }
+
+ let deltaY;
+ if (yPrev == undefined) {
+ deltaY = yNext - z0;
+ } else if (yNext == undefined) {
+ deltaY = yPrev - z0;
+ } else {
+ deltaY = (yNext - yPrev) / 2;
+ }
+
+ // Calculamos la altura del punto en el espacio 3D
+ const z = amplitude * z0;
+
+ // Añadimos los valores de los puntos al array de posiciones
+ positions.push((width * i) / widthSegments - width / 2);
+ positions.push(z);
+ positions.push((height * j) / heightSegments - height / 2);
+
+ // Calculamos los vectores tangentes a la superficie en el ejex y en el eje y
+ let tanX = new THREE.Vector3(width / widthSegments, deltaX * amplitude, 0).normalize();
+ let tanY = new THREE.Vector3(0, deltaY * amplitude, height / heightSegments).normalize();
+
+ // Calculamos el vector normal a la superficie
+ let n = new THREE.Vector3();
+ n.crossVectors(tanY, tanX);
+
+ // Añadimos los valores de los vectores normales al array de normales
+ normals.push(n.x);
+ normals.push(n.y);
+ normals.push(n.z);
+
+ uvs.push(i / (widthSegments - 1));
+ uvs.push(j / (heightSegments - 1));
+
+ if (i == widthSegments - 2 || j == heightSegments - 2) continue;
+
+ // Ensamblamos los triangulos
+ indices.push(i + j * quadsPerRow);
+ indices.push(i + 1 + j * quadsPerRow);
+ indices.push(i + 1 + (j + 1) * quadsPerRow);
+
+ indices.push(i + j * quadsPerRow);
+ indices.push(i + 1 + (j + 1) * quadsPerRow);
+ indices.push(i + (j + 1) * quadsPerRow);
+ }
+ }
+
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
+ geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
+ geometry.setIndex(indices);
+
+ return geometry;
+}
+
+function buildScene() {
+ console.log('Building scene');
+
+ const width = 100;
+ const height = 100;
+
+ terrainGeometry = elevationGeometry(
+ width, height,
+ amplitude,
+ widthSegments, heightSegments,
+ textures.elevationMap.object);
+
+ console.log('Applying textures');
+ terrainMaterial = new THREE.RawShaderMaterial({
+ uniforms: {
+ dirtSampler: { type: 't', value: textures.tierra.object },
+ rockSampler: { type: 't', value: textures.roca.object },
+ grassSampler: { type: 't', value: textures.pasto.object },
+ scale: { type: 'f', value: 3.0 },
+ terrainAmplitude: { type: 'f', value: amplitude },
+ terrainAmplitudeBottom: { type: 'f', value: amplitudeBottom },
+ worldNormalMatrix: { type: 'm4', value: null },
+ dirtStepWidth: { type: 'f', value: 0.20 },
+ rockStepWidth: { type: 'f', value: 0.15 },
+ },
+ vertexShader: vertexShader,
+ fragmentShader: fragmentShader,
+ side: THREE.DoubleSide,
+ });
+ terrainMaterial.needsUpdate = true;
+
+ terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
+ terrain.position.set(0, amplitudeBottom, 0);
+ scene.add(terrain);
+
+ console.log('Generating water');
+ const waterGeometry = new THREE.PlaneGeometry(width/2, height);
+ const waterMaterial = new THREE.MeshPhongMaterial( {color: 0x12ABFF, side: THREE.DoubleSide} );
+ const water = new THREE.Mesh( waterGeometry, waterMaterial );
+ water.rotateX(Math.PI/2);
+ water.position.set(0, 0.75, 0);
+ scene.add(water);
+
+ const [treeLogs, treeLeaves] = createInstancedTrees(100);
+ scene.add(treeLogs);
+ scene.add(treeLeaves);
+}
+
+function onTextureLoaded(key, texture) {
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+ textures[key].object = texture;
+ console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+ const loadingManager = new THREE.LoadingManager();
+
+ loadingManager.onLoad = () => {
+ console.log('All textures loaded');
+ callback();
+ };
+
+ for (const key in textures) {
+ console.log("Loading textures");
+ const loader = new THREE.TextureLoader(loadingManager);
+ const texture = textures[key];
+ texture.object = loader.load(
+ texture.url,
+ onTextureLoaded.bind(this, key),
+ null,
+ (error) => {
+ console.error(error);
+ }
+ );
+ }
+}
+
+function createMenu() {
+ const gui = new dat.GUI({ width: 400 });
+ gui.add(terrainMaterial.uniforms.scale, 'value', 1.00, 5.00).name('Terrain texture scale');
+ gui.add(terrainMaterial.uniforms.dirtStepWidth, 'value', 0.0, 1.0).name('dirt step width');
+ gui.add(terrainMaterial.uniforms.rockStepWidth, 'value', 0.10, 0.50).name('rock step width');
+}
+
+function mainLoop() {
+ requestAnimationFrame(mainLoop);
+ renderer.render(scene, camera);
+}
+
+setupThreeJs();
+loadTextures(main);
+
+function main() {
+ buildScene();
+ createMenu();
+ mainLoop();
+}
diff --git a/tp/src/standalone/track-map.js b/tp/src/standalone/track-map.js
@@ -0,0 +1,423 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { vertexShader, fragmentShader } from '/assets/shaders.js';
+
+let scene, camera, renderer, container, terrainMaterial, terrainGeometry, terrain;
+let treesForbiddenMapData, treesForbiddenMap, elevationMap, elevationMapData;
+
+const widthSegments = 100;
+const heightSegments = 100;
+const amplitude = 8;
+const amplitudeBottom = -1.00;
+
+const textures = {
+ tierra: { url: '/assets/tierra.jpg', object: null },
+ roca: { url: '/assets/roca.jpg', object: null },
+ pasto: { url: '/assets/pasto.jpg', object: null },
+ elevationMap: { url: '/assets/elevation_map2.png', object: null },
+ treeForbiddenMap: { url: '/assets/tree_forbidden_zone_map.png', object: null }
+};
+
+function onResize() {
+ camera.aspect = container.offsetWidth / container.offsetHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+ scene = new THREE.Scene();
+ container = document.getElementById('mainContainer');
+
+ renderer = new THREE.WebGLRenderer();
+ renderer.setClearColor(0x606060);
+ container.appendChild(renderer.domElement);
+
+ camera = new THREE.PerspectiveCamera(
+ 35, window.innerWidth/window.innerHeight, 0.1, 1000);
+ camera.position.set(100, 120, -100);
+ camera.lookAt(0, 0, 0);
+
+ const controls = new OrbitControls(camera, renderer.domElement);
+
+ const ambientLight = new THREE.AmbientLight(0xffffff);
+ scene.add(ambientLight);
+
+ const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+ //scene.add(hemisphereLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(100, 100, 100);
+ scene.add(directionalLight);
+
+ const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
+ // scene.add(directionalLightHelper);
+
+ const gridHelper = new THREE.GridHelper(150, 150);
+ scene.add(gridHelper);
+
+ const axesHelper = new THREE.AxesHelper( 5 );
+ scene.add( axesHelper );
+
+ window.addEventListener('resize', onResize);
+ onResize();
+}
+
+const imgWidth = 512;
+const imgHeight = 512;
+
+// (x, y) ∈ [imgHeight, imgWidth] -> son un punto de la imagen
+function getPixelIndex(x, y) {
+ return Math.floor(x + y*imgWidth*4);
+}
+
+function getPixel(imgData, index) {
+ let i = index*4, d = imgData.data
+ return [d[i],d[i+1],d[i+2],d[i+3]] // Returns array [R,G,B,A]
+}
+
+function getPixelXY(imgData, x, y) {
+ return getPixel(imgData, y*imgData.width+x)
+}
+
+// position: Vector3
+function isForbbidenPosition(position) {
+ const x = Math.floor(position.x);
+ const y = position.y;
+ const z = Math.floor(position.z);
+
+ /*
+ if((y > 5.0) || (y < 2.65)){
+ console.log("(" + position.x + ", " + position.y + ", " + position.z + ") is not valid ");
+ return true;
+ }
+ */
+
+ let pixelArray = getPixelXY(treesForbiddenMap, x, z);
+ const R = pixelArray[0]; // Red
+ const G = pixelArray[1]; // Green
+ const B = pixelArray[2]; // Blue
+ const A = pixelArray[3]; // Alpha
+ // const pixel = new THREE.Vector4(R, G, B, A);
+
+ if(((R <= 10) && (G >= 250) && (B <= 10))
+ || (R <= 80) && (G <= 80) && (B <= 80)
+ || (R >= 200) && (G >= 200) && (B >= 200)) {
+ // console.log("(" + position.x + ", " + position.y + ", " + position.z + ") is not valid ");
+ return true;
+ }
+
+ console.log("(" + position.x + ", " + position.y + ") is valid ");
+ return false;
+}
+
+// obtiene una posicion aleatoria en el terreno, para obtener la altura del
+// terreno utiliza el mapa de elevacion.
+// `padding` permite definir un borde del cual no se toman puntos
+function getRandomPositionInTerrain(padding = 0) {
+ const x = Math.floor(Math.random() * (widthSegments-(padding*2)));
+ const z = Math.floor(Math.random() * (heightSegments-(padding*2)));
+
+ const pixelArray = getPixelXY(elevationMap, x, z); // array [R,G,B,A]
+ const y = (pixelArray[0]/255)*amplitude;
+
+ const position = new THREE.Vector3(x+padding, y, z+padding);
+ return position;
+}
+
+function createInstancedTrees(count) {
+ console.log('Generating `' + count + '` instances of tree');
+
+ let logHeight = 4.0;
+ const treeLogGeometry = new THREE.CylinderGeometry(
+ 0.30, 0.30, logHeight, 40, 40);
+ treeLogGeometry.translate(0, logHeight/2.0, 0);
+ const instancedTreeLogGeometry = new THREE.InstancedBufferGeometry();
+ instancedTreeLogGeometry.copy(treeLogGeometry);
+ const treeLogMaterial = new THREE.MeshPhongMaterial({color: 0x7c3f00});
+ const instancedTreeLogs = new THREE.InstancedMesh(
+ instancedTreeLogGeometry,
+ treeLogMaterial,
+ count);
+
+ const treeLeavesGeometry = new THREE.SphereGeometry(1.75,40,40);
+ const instancedTreeLeavesGeometry = new THREE.InstancedBufferGeometry();
+ instancedTreeLeavesGeometry.copy(treeLeavesGeometry);
+ const treeLeavesMaterial = new THREE.MeshPhongMaterial({color: 0x365829});
+ const instancedTreeLeaves = new THREE.InstancedMesh(
+ instancedTreeLeavesGeometry,
+ treeLeavesMaterial,
+ count);
+
+ const rotMatrix = new THREE.Matrix4();
+ const translationMatrix = new THREE.Matrix4();
+ const treeLogMatrix = new THREE.Matrix4();
+ const treeLeavesMatrix = new THREE.Matrix4();
+
+ const treesBorderPadding = 3.0;
+ for (let i = 0; i < count; i++) {
+ let position = getRandomPositionInTerrain(treesBorderPadding);
+ for(let j = 0; isForbbidenPosition(position); ++j) {
+ position = getRandomPositionInTerrain(treesBorderPadding);
+ if(j++ == 1000) { // maximo de iteraciones
+ break;
+ }
+ }
+
+ if(isForbbidenPosition(position)) {
+ continue;
+ }
+
+ const treeOffset = 0.25;
+ // 1.50 numbero magico para posicionar correctamente los arboles con
+ // respecto al terreno
+ position.x -= (widthSegments+treesBorderPadding+1.50)/2;
+ position.y += amplitudeBottom - treeOffset;
+ position.z -= (heightSegments+treesBorderPadding)/2;
+ translationMatrix.makeTranslation(position);
+ treeLogMatrix.identity();
+ treeLeavesMatrix.identity();
+
+ let scale = 0.6 + (Math.random()*(logHeight/3));
+ treeLogMatrix.makeScale(1, scale, 1);
+ treeLogMatrix.premultiply(translationMatrix);
+
+ position.y += scale*logHeight;
+ translationMatrix.makeTranslation(position);
+ treeLeavesMatrix.premultiply(translationMatrix);
+
+ instancedTreeLogs.setMatrixAt(i, treeLogMatrix);
+ instancedTreeLeaves.setMatrixAt(i, treeLeavesMatrix);
+ }
+
+ console.log('Done generating `' + count + '` instances of tree');
+ return [instancedTreeLogs, instancedTreeLeaves];
+}
+
+
+// La funcion devuelve una geometria de Three.js
+// width: Ancho del plano
+// height: Alto del plano
+// amplitude: Amplitud de la elevacion
+// widthSegments: Numero de segmentos en el ancho
+// heightSegments: Numero de segmentos en el alto
+function elevationGeometry(width, height, amplitude, widthSegments, heightSegments) {
+ console.log('Generating terrain geometry');
+ let geometry = new THREE.BufferGeometry();
+
+ const positions = [];
+ const indices = [];
+ const normals = [];
+ const uvs = [];
+
+ let imageData = elevationMap;
+ let data = elevationMapData;
+
+ const quadsPerRow = widthSegments - 1;
+
+ // Recorremos los segmentos horizontales y verticales
+ for (let x = 0; x < widthSegments - 1; x++) {
+ for (let y = 0; y < heightSegments - 1; y++) {
+ // valor del pixel en el mapa de elevacion en la posicion i, j
+
+ let pixel = getPixelXY(imageData, x, y);
+
+ // se utiliza el canal rojo [R, G, B, A];
+ let z0 = pixel[0] / 255;
+ const z = amplitude * z0;
+
+ // valores de los píxeles de los puntos adyacentes
+ let xPrev, xNext, yPrev, yNext;
+
+ xPrev = (x > 0) ? getPixelXY(imageData, x-1, y)[0]/255 : undefined;
+ xNext = (x < widthSegments-1) ? getPixelXY(imageData, x+1, y)[0]/255 : undefined;
+
+ yPrev = (y > 0) ? getPixelXY(imageData, x, y+1)[0]/255 : undefined;
+ yNext = (y < heightSegments-1) ? getPixelXY(imageData, x, y+1)[0]/255 : undefined;
+
+ // diferencia entre los valores de los píxeles adyacentes en el eje
+ // `x` y en el eje `y` de la imagen en el espacio de la textura
+ let deltaX;
+ if (xPrev == undefined) {
+ deltaX = xNext - z0;
+ } else if (yNext == undefined) {
+ deltaX = xPrev - z0;
+ } else {
+ deltaX = (xNext - xPrev) / 2;
+ }
+
+ let deltaY;
+ if (yPrev == undefined) {
+ deltaY = yNext - z0;
+ } else if (yNext == undefined) {
+ deltaY = yPrev - z0;
+ } else {
+ deltaY = (yNext - yPrev) / 2;
+ }
+
+ // Añadimos los valores de los puntos al array de posiciones
+ positions.push((width * x) / widthSegments - width / 2);
+ positions.push(z);
+ positions.push((height * y) / heightSegments - height / 2);
+
+ // vectores tangentes a la superficie en el eje `x` y en el eje `y`
+ let tanX = new THREE.Vector3(width / widthSegments, deltaX * amplitude, 0);
+ let tanY = new THREE.Vector3(0, deltaY * amplitude, height / heightSegments);
+
+ tanX.normalize();
+ tanY.normalize();
+
+ let normal = new THREE.Vector3().crossVectors(tanY, tanX);
+
+ normals.push(normal.x);
+ normals.push(normal.y);
+ normals.push(normal.z);
+
+ uvs.push(x / (widthSegments - 1));
+ uvs.push(y / (heightSegments - 1));
+
+ if ((x == widthSegments-2) || (y == heightSegments-2)) continue;
+
+ // Ensamblamos los triangulos
+ indices.push(x + y*quadsPerRow);
+ indices.push(x + 1 + y*quadsPerRow);
+ indices.push(x + 1 + (y+1)*quadsPerRow);
+
+ indices.push(x + y*quadsPerRow);
+ indices.push(x + 1 + (y+1)*quadsPerRow);
+ indices.push(x + (y+1)*quadsPerRow);
+ }
+ }
+
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
+ geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
+ geometry.setIndex(indices);
+
+ return geometry;
+}
+
+function buildScene() {
+ console.log('Building scene');
+
+ const width = widthSegments;
+ const height = heightSegments;
+
+ terrainGeometry = elevationGeometry(
+ width, height,
+ amplitude,
+ widthSegments, heightSegments);
+
+ console.log('Applying textures');
+ terrainMaterial = new THREE.RawShaderMaterial({
+ uniforms: {
+ dirtSampler: { type: 't', value: textures.tierra.object },
+ rockSampler: { type: 't', value: textures.roca.object },
+ grassSampler: { type: 't', value: textures.pasto.object },
+ scale: { type: 'f', value: 3.0 },
+ terrainAmplitude: { type: 'f', value: amplitude },
+ terrainAmplitudeBottom: { type: 'f', value: amplitudeBottom },
+ worldNormalMatrix: { type: 'm4', value: null },
+ dirtStepWidth: { type: 'f', value: 0.20 },
+ rockStepWidth: { type: 'f', value: 0.15 },
+ },
+ vertexShader: vertexShader,
+ fragmentShader: fragmentShader,
+ side: THREE.DoubleSide,
+ });
+ terrainMaterial.needsUpdate = true;
+
+ terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
+ terrain.position.set(0, amplitudeBottom, 0);
+ scene.add(terrain);
+
+ const waterGeometry = new THREE.PlaneGeometry(width/2, height-1.55);
+ const waterMaterial = new THREE.MeshPhongMaterial( {color: 0x12ABFF, side: THREE.DoubleSide} );
+ const water = new THREE.Mesh( waterGeometry, waterMaterial );
+ water.rotateX(Math.PI/2);
+ water.position.set(0, 0.75, -1.00);
+ scene.add(water);
+
+ const [treeLogs, treeLeaves] = createInstancedTrees(250);
+ scene.add(treeLogs);
+ scene.add(treeLeaves);
+}
+
+function onTextureLoaded(key, texture) {
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+ textures[key].object = texture;
+ console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+ const loadingManager = new THREE.LoadingManager();
+
+ loadingManager.onLoad = () => {
+ console.log('All textures loaded');
+ callback();
+ };
+
+ for (const key in textures) {
+ console.log("Loading textures");
+ const loader = new THREE.TextureLoader(loadingManager);
+ const texture = textures[key];
+ texture.object = loader.load(
+ texture.url,
+ onTextureLoaded.bind(this, key),
+ null,
+ (error) => {
+ console.error(error);
+ }
+ );
+ }
+}
+
+function createMenu() {
+ const gui = new dat.GUI({ width: 400 });
+ gui.add(terrainMaterial.uniforms.scale, 'value', 1.00, 5.00).name('Terrain texture scale');
+ gui.add(terrainMaterial.uniforms.dirtStepWidth, 'value', 0.0, 1.0).name('dirt step width');
+ gui.add(terrainMaterial.uniforms.rockStepWidth, 'value', 0.10, 0.50).name('rock step width');
+}
+
+function mainLoop() {
+ requestAnimationFrame(mainLoop);
+ renderer.render(scene, camera);
+}
+
+function loadMapsData() {
+ console.log("Loading maps data");
+
+ // Creamos un canvas para poder leer los valores de los píxeles de la textura
+ let canvas = document.createElement('canvas');
+ let ctx = canvas.getContext('2d');
+
+ let treesForbiddenMapImage = textures.treeForbiddenMap.object.image;
+ let elevationMapImage = textures.elevationMap.object.image;
+
+ // ambos mapas deben tener el mismo tamaño
+ const imgWidth = widthSegments;
+ const imgHeight = heightSegments;
+
+ canvas.width = imgWidth;
+ canvas.height = imgHeight;
+
+ ctx.drawImage(treesForbiddenMapImage, 0, 0, imgWidth, imgHeight);
+ treesForbiddenMap = ctx.getImageData(0, 0, imgWidth, imgHeight);
+ treesForbiddenMapData = treesForbiddenMap.data;
+
+ ctx.drawImage(elevationMapImage, 0, 0, imgWidth, imgHeight);
+ elevationMap = ctx.getImageData(0, 0, imgWidth, imgHeight);
+ elevationMapData = elevationMap.data
+
+ console.log("All maps data loaded succesfully");
+}
+
+function main() {
+ loadMapsData();
+ buildScene();
+ // getTrainTrackShape();
+ mainLoop();
+}
+
+setupThreeJs();
+loadTextures(main);
diff --git a/tp/src/standalone/train.js b/tp/src/standalone/train.js
@@ -0,0 +1,338 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
+
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+
+let scene, camera, renderer, container, terrainMaterial, instancedTrees;
+
+let time = 0.0;
+
+function onResize() {
+ camera.aspect = container.offsetWidth / container.offsetHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+ scene = new THREE.Scene();
+ container = document.getElementById('mainContainer');
+
+ renderer = new THREE.WebGLRenderer();
+ renderer.setClearColor(0x606060);
+ container.appendChild(renderer.domElement);
+
+ camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000);
+ camera.position.set(-50, 60, 50);
+ camera.lookAt(0, 0, 0);
+
+ const controls = new OrbitControls(camera, renderer.domElement);
+
+ const ambientLight = new THREE.AmbientLight(0xAAAAAA);
+ scene.add(ambientLight);
+
+ const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+ scene.add(hemisphereLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(100, 100, 100);
+ scene.add(directionalLight);
+
+ const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
+ // scene.add(directionalLightHelper);
+
+ const gridHelper = new THREE.GridHelper(50, 20);
+ scene.add(gridHelper);
+
+ const axesHelper = new THREE.AxesHelper(5);
+ scene.add(axesHelper);
+
+ window.addEventListener('resize', onResize);
+ onResize();
+}
+
+const steamChamberLen = 20;
+const steamChamberRad = 5;
+const steamChamberEndRad = steamChamberRad+0.75;
+const steamChamberEndLen = 5;
+const cabinLen = 10;
+const cabinHeight = 11;
+const cabinRoofHeight = 8;
+const cabinWallThickness = 0.75;
+const wheelRad = 2.75;
+const chassisHeight = 5;
+const wheelThickness = 0.85;
+const chassisOffset = 2.49;
+const wheelOffset = -0.70;
+const steamCylindersLen = 8;
+const crankLen = 22;
+const crankOffset = 3.750;
+const crankWidth = 0.5;
+
+let crankLeft, crankRight;
+
+function buildCabinRoof() {
+ console.log('Building train cabin roof');
+ const geometry = new THREE.BoxGeometry(12, cabinWallThickness, 12);
+ return geometry;
+}
+
+function buildCabin() {
+ console.log('Building train cabin');
+
+ let cabin = [];
+
+ const cabinFront = new THREE.BoxGeometry(steamChamberRad*2, cabinWallThickness, cabinHeight);
+ cabinFront.translate(0, cabinLen/2, -cabinHeight/2);
+ cabin.push(cabinFront);
+
+ const cabinLeft = new THREE.BoxGeometry(steamChamberRad*2, cabinWallThickness, cabinHeight);
+ cabinLeft.rotateZ(Math.PI/2);
+ cabinLeft.translate(steamChamberRad-cabinWallThickness/2, cabinWallThickness/2, -cabinHeight/2);
+ cabin.push(cabinLeft);
+
+ const cabinRight = new THREE.BoxGeometry(steamChamberRad*2, cabinWallThickness, cabinHeight);
+ cabinRight.rotateZ(Math.PI/2);
+ cabinRight.translate(-steamChamberRad+cabinWallThickness/2, cabinWallThickness/2, -cabinHeight/2);
+ cabin.push(cabinRight);
+
+ const g1 = new THREE.BoxGeometry(cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+ g1.rotateZ(Math.PI/2);
+ g1.translate(-steamChamberRad+(cabinWallThickness/2), -steamChamberRad+cabinWallThickness, -cabinHeight-cabinRoofHeight/2);
+ cabin.push(g1);
+
+ const g2 = new THREE.BoxGeometry(cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+ g2.rotateZ(Math.PI/2);
+ g2.translate(steamChamberRad-cabinWallThickness/2, steamChamberRad, -cabinHeight-cabinRoofHeight/2);
+ cabin.push(g2);
+
+ const g3 = new THREE.BoxGeometry(cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+ g3.rotateZ(Math.PI/2);
+ g3.translate(steamChamberRad-cabinWallThickness/2, -steamChamberRad+cabinWallThickness, -cabinHeight-cabinRoofHeight/2);
+ cabin.push(g3);
+
+ const g4 = new THREE.BoxGeometry(cabinWallThickness, cabinWallThickness, cabinRoofHeight);
+ g4.rotateZ(Math.PI/2);
+ g4.translate(-steamChamberRad+cabinWallThickness/2, steamChamberRad, -cabinHeight-cabinRoofHeight/2);
+ cabin.push(g4);
+
+ const geometry = BufferGeometryUtils.mergeGeometries(cabin);
+ geometry.rotateX(Math.PI/2);
+ return geometry;
+}
+
+function buildChamber() {
+ let geometries = [];
+
+ const steamChamber = new THREE.CylinderGeometry(steamChamberRad,
+ steamChamberRad, steamChamberLen, 32);
+
+ geometries.push(steamChamber);
+
+ const steamChamberEnd = new THREE.CylinderGeometry(steamChamberEndRad,
+ steamChamberEndRad, steamChamberEndLen, 32);
+
+ steamChamberEnd.translate(0,steamChamberLen/2 + steamChamberEndLen/2,0);
+ geometries.push(steamChamberEnd);
+
+ const floor = new THREE.BoxGeometry(steamChamberRad*2, steamChamberLen + steamChamberEndLen + cabinLen, 1.0);
+ floor.translate(0, -steamChamberEndLen/2, steamChamberRad);
+ geometries.push(floor);
+
+ const chamberPipeLen = 8;
+ const chamberPipe = new THREE.CylinderGeometry(0.75, 0.75, chamberPipeLen, 32);
+ chamberPipe.translate(0, -(steamChamberRad + chamberPipeLen/2)+1.0,
+ -(steamChamberLen+steamChamberEndLen)/2);
+ chamberPipe.rotateX(Math.PI/2);
+ geometries.push(chamberPipe);
+
+ const geometry = BufferGeometryUtils.mergeGeometries(geometries);
+ geometry.rotateX(Math.PI/2);
+ geometry.translate(0, steamChamberRad, 0);
+ return geometry;
+}
+
+function buildTrainWheel() {
+ const wheel = new THREE.CylinderGeometry(wheelRad, wheelRad, wheelThickness);
+ wheel.rotateZ(Math.PI/2);
+
+ const wheelBolt = new THREE.CylinderGeometry(wheelRad, wheelRad, wheelThickness);
+ wheelBolt.rotateZ(Math.PI/2);
+
+ const wheelsMaterial = new THREE.MeshPhongMaterial({
+ color: 0x393939,
+ side: THREE.DoubleSide,
+ shininess: 100.0
+ });
+
+ return new THREE.Mesh(wheel, wheelsMaterial)
+}
+
+function buildTrainAxe(material) {
+ const axeGeometry = new THREE.CylinderGeometry(0.65, 0.65, 10);
+ axeGeometry.rotateZ(Math.PI/2);
+
+ const axeMaterial = new THREE.MeshPhongMaterial({
+ color: 0x7A7F80,
+ side: THREE.DoubleSide,
+ shininess: 100.0
+ });
+
+ return new THREE.Mesh(axeGeometry, axeMaterial);
+}
+
+function buildTrainChassis() {
+ const chassis = new THREE.BoxGeometry(7, 5, steamChamberLen+steamChamberEndLen+cabinLen);
+ return chassis;
+}
+
+function buildTrain() {
+ console.log('Building train');
+ const train = new THREE.Group();
+
+ const chassisGeometry = buildTrainChassis();
+ const chassisMaterial = new THREE.MeshPhongMaterial({
+ color: 0x7A7F80,
+ side: THREE.DoubleSide,
+ shininess: 100.0
+ });
+
+ const chassis = new THREE.Mesh(chassisGeometry, chassisMaterial);
+ train.add(chassis);
+
+ const chamberGeometry = buildChamber();
+ const chamberMaterial = new THREE.MeshPhongMaterial({
+ color: 0xFA1A09,
+ side: THREE.DoubleSide,
+ shininess: 100.0
+ });
+
+ const chamber = new THREE.Mesh(chamberGeometry, chamberMaterial);
+ chassis.add(chamber);
+ chamber.position.set(0, (chassisHeight + cabinWallThickness)/2, chassisOffset);
+
+ const cabinGeometry = buildCabin();
+ const cabin = new THREE.Mesh(cabinGeometry, chamberMaterial);
+ chassis.add(cabin);
+ cabin.position.set(0, (chassisHeight + cabinWallThickness)/2, -steamChamberLen+(cabinLen/2)+chassisOffset);
+
+ const cabinRoofGeometry = buildCabinRoof();
+ const roofMaterial = new THREE.MeshPhongMaterial({
+ color: 0xFBEC50,
+ side: THREE.DoubleSide,
+ shininess: 100.0
+ });
+
+ const cabinRoof = new THREE.Mesh(cabinRoofGeometry, roofMaterial);
+ cabin.add(cabinRoof);
+ cabinRoof.position.set(0, cabinHeight+cabinRoofHeight+cabinWallThickness/2, 0);
+
+ const a1 = buildTrainAxe();
+ chassis.add(a1);
+
+ const a2 = buildTrainAxe();
+ chassis.add(a2);
+
+ const a3 = buildTrainAxe(chassisMaterial);
+ chassis.add(a3);
+
+ a1.position.set(0, wheelOffset, 0);
+ a2.position.set(0, wheelOffset, wheelRad*2.5);
+ a3.position.set(0, wheelOffset, -wheelRad*2.5);
+
+
+ const cylinderLeft = new THREE.CylinderGeometry(2.25, 2.5, steamCylindersLen);
+ cylinderLeft.rotateX(Math.PI/2);
+ cylinderLeft.translate(steamChamberRad-0.25, 0, steamChamberLen-steamCylindersLen/1.5);
+
+ const cylinderRight = new THREE.CylinderGeometry(2.25, 2.5, steamCylindersLen);
+ cylinderRight.rotateX(Math.PI/2);
+ cylinderRight.translate(-steamChamberRad+0.25, 0, steamChamberLen-steamCylindersLen/1.5);
+
+ const cylindersGeometry = BufferGeometryUtils.mergeGeometries([cylinderRight, cylinderLeft]);
+ const cylindersMaterial = new THREE.MeshPhongMaterial({
+ color: 0x393939,
+ side: THREE.DoubleSide,
+ shininess: 100.0
+ });
+
+ chassis.add(new THREE.Mesh(cylindersGeometry, cylindersMaterial));
+ chassis.position.set(0,-2,-2.75);
+
+ const w1 = buildTrainWheel();
+ w1.position.set(steamChamberRad-wheelThickness/2.1,0,0);
+ a1.add(w1);
+
+ const w2 = buildTrainWheel();
+ w2.position.set(-steamChamberRad+wheelThickness/2.1,0,0);
+ a1.add(w2);
+
+ const w3 = buildTrainWheel();
+ w3.position.set(steamChamberRad-wheelThickness/2.1,0,0);
+ a2.add(w3);
+
+ const w4 = buildTrainWheel();
+ w4.position.set(-steamChamberRad+wheelThickness/2.1,0,);
+ a2.add(w4);
+
+ const w5 = buildTrainWheel();
+ w5.position.set(steamChamberRad-wheelThickness/2.1,0,0);
+ a3.add(w5);
+
+ const w6 = buildTrainWheel();
+ w6.position.set(-steamChamberRad+wheelThickness/2.1,0,0);
+ a3.add(w6);
+
+ const crankGeometry = new THREE.BoxGeometry(crankWidth, 1.0, crankLen);
+
+ crankRight = new THREE.Mesh(crankGeometry, chassisMaterial);
+ //crankRight.position.set(steamChamberRad, wheelOffset, crankOffset);
+
+ crankLeft = new THREE.Mesh(crankGeometry, chassisMaterial);
+ //crankLeft.position.set(-steamChamberRad, wheelOffset, crankOffset);
+
+ chassis.add(crankLeft);
+ chassis.add(crankRight);
+
+ chassis.translateY(-wheelOffset);
+
+ train.position.set(0, 2, 0);
+ return train;
+}
+
+function buildScene() {
+ console.log('Building scene');
+
+ const train = buildTrain();
+ scene.add(train);
+}
+
+function onTextureLoaded(key, texture) {
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+ textures[key].object = texture;
+ console.log('Texture `' + key + '` loaded');
+}
+
+function mainLoop() {
+ time += 0.05;
+
+ requestAnimationFrame(mainLoop);
+
+ crankLeft.position.set(-steamChamberRad-crankWidth/2,
+ wheelOffset+1.00*(Math.sin(time*Math.PI/2)),
+ crankOffset - 1.00*(Math.cos(time*Math.PI/2)));
+
+ crankRight.position.set(steamChamberRad+crankWidth/2,
+ wheelOffset+1.00*(Math.sin(time*Math.PI/2)),
+ crankOffset - 1.00*(Math.cos(time*Math.PI/2)));
+
+ renderer.render(scene, camera);
+}
+
+function main() {
+ buildScene();
+ mainLoop();
+}
+
+setupThreeJs();
+main();
diff --git a/tp/src/standalone/trees.js b/tp/src/standalone/trees.js
@@ -0,0 +1,204 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { vertexShader, fragmentShader } from '/assets/treesShaders.js';
+
+let scene, camera, renderer, container, terrainMaterial, instancedTrees;
+
+const textures = {
+ tierra: { url: '/assets/tierra.jpg', object: null },
+ roca: { url: '/assets/roca.jpg', object: null },
+ pasto: { url: '/assets/pasto.jpg', object: null },
+};
+
+function onResize() {
+ camera.aspect = container.offsetWidth / container.offsetHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+ scene = new THREE.Scene();
+ container = document.getElementById('mainContainer');
+
+ renderer = new THREE.WebGLRenderer();
+ renderer.setClearColor(0x606060);
+ container.appendChild(renderer.domElement);
+
+ camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000);
+ camera.position.set(-50, 60, 50);
+ camera.lookAt(0, 0, 0);
+
+ const controls = new OrbitControls(camera, renderer.domElement);
+
+ const ambientLight = new THREE.AmbientLight(0xffffff);
+ scene.add(ambientLight);
+
+ const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+ scene.add(hemisphereLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(100, 100, 100);
+ scene.add(directionalLight);
+
+ const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
+ // scene.add(directionalLightHelper);
+
+ //const gridHelper = new THREE.GridHelper(50, 20);
+ //scene.add(gridHelper);
+
+ const axesHelper = new THREE.AxesHelper(5);
+ scene.add(axesHelper);
+
+ window.addEventListener('resize', onResize);
+ onResize();
+}
+
+function createInstancedTrees(count) {
+ console.log('Generating `' + count + '` instances of tree');
+
+ let logHeight = 4.0;
+
+ const treeLogGeometry = new THREE.CylinderGeometry(0.30, 0.30, logHeight, 40, 40);
+ const treeLeavesGeometry = new THREE.SphereGeometry(1.75,40,40);
+
+ treeLogGeometry.translate(0, logHeight/2.0, 0);
+
+ const instancedTreeLogGeometry = new THREE.InstancedBufferGeometry();
+ instancedTreeLogGeometry.copy(treeLogGeometry);
+
+ const instancedTreeLeavesGeometry = new THREE.InstancedBufferGeometry();
+ instancedTreeLeavesGeometry.copy(treeLeavesGeometry);
+
+ const treeLogMaterial = new THREE.MeshPhongMaterial({color: 0x7c3f00});
+ const instancedTreeLogs = new THREE.InstancedMesh(instancedTreeLogGeometry, treeLogMaterial, count);
+
+ const treeLeavesMaterial = new THREE.MeshPhongMaterial({color: 0x365829});
+ const instancedTreeLeaves = new THREE.InstancedMesh(instancedTreeLeavesGeometry, treeLeavesMaterial, count);
+
+ const rotMatrix = new THREE.Matrix4();
+
+ const translationMatrix = new THREE.Matrix4();
+ const treeLogMatrix = new THREE.Matrix4();
+ const treeLeavesMatrix = new THREE.Matrix4();
+
+ //let origin = new THREE.Vector3();
+ const RANGE = 50 - 4/2;
+
+ for (let i = 0; i < count; i++) {
+ let position = new THREE.Vector3(
+ (Math.random() - 0.5) * RANGE,
+ 0,
+ (Math.random() - 0.5) * RANGE
+ );
+
+ translationMatrix.makeTranslation(position);
+
+ //rotMatrix.lookAt(0, 0, new THREE.Vector3(0, 1, 0));
+ treeLogMatrix.identity();
+ treeLeavesMatrix.identity();
+
+ let scale = 0.5 + (Math.random()*(logHeight/3));
+ treeLogMatrix.makeScale(1, scale, 1);
+ //matrix.premultiply(rotMatrix);
+
+ treeLogMatrix.premultiply(translationMatrix);
+
+ position.y = scale*logHeight;
+ translationMatrix.makeTranslation(position);
+ treeLeavesMatrix.premultiply(translationMatrix);
+
+ instancedTreeLogs.setMatrixAt(i, treeLogMatrix);
+ instancedTreeLeaves.setMatrixAt(i, treeLeavesMatrix);
+ }
+
+ scene.add(instancedTreeLogs);
+ scene.add(instancedTreeLeaves);
+}
+
+function buildScene() {
+ console.log('Building scene');
+
+ console.log('Generating terrain');
+ const terrainGeometry = new THREE.PlaneGeometry(50, 50);
+ //const terrainMaterial = new THREE.MeshPhongMaterial( {color: 0x365829, side: THREE.DoubleSide} );
+ terrainMaterial = new THREE.RawShaderMaterial({
+ uniforms: {
+ tierraSampler: { type: 't', value: textures.tierra.object },
+ rocaSampler: { type: 't', value: textures.roca.object },
+ pastoSampler: { type: 't', value: textures.pasto.object },
+ scale1: { type: 'f', value: 2.0 },
+
+ mask1low: { type: 'f', value: -0.38 },
+ mask1high: { type: 'f', value: 0.1 },
+
+ mask2low: { type: 'f', value: 0.05 },
+ mask2high: { type: 'f', value: -0.70 },
+ },
+ vertexShader: vertexShader,
+ fragmentShader: fragmentShader,
+ side: THREE.DoubleSide,
+ });
+ terrainMaterial.needsUpdate = true;
+
+ const terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
+ terrain.rotateX(Math.PI/2);
+ terrain.position.set(0, 0, 0);
+ scene.add(terrain);
+
+ console.log('Generating trees');
+ createInstancedTrees(35);
+}
+
+function onTextureLoaded(key, texture) {
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+ textures[key].object = texture;
+ console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+ const loadingManager = new THREE.LoadingManager();
+
+ loadingManager.onLoad = () => {
+ console.log('All textures loaded');
+ callback();
+ };
+
+ for (const key in textures) {
+ console.log("Loading textures");
+ const loader = new THREE.TextureLoader(loadingManager);
+ const texture = textures[key];
+ texture.object = loader.load(
+ texture.url,
+ onTextureLoaded.bind(this, key),
+ null,
+ (error) => {
+ console.error(error);
+ }
+ );
+ }
+}
+
+
+function createMenu() {
+ const gui = new dat.GUI({ width: 400 });
+ gui.add(terrainMaterial.uniforms.scale1, 'value', 0, 10).name('Texture scale');
+ gui.add(terrainMaterial.uniforms.mask1low, 'value', -1, 1).name('Mask1 Low');
+ gui.add(terrainMaterial.uniforms.mask1high, 'value', -1, 1).name('Mask1 High');
+ gui.add(terrainMaterial.uniforms.mask2low, 'value', -1, 1).name('Mask2 Low');
+ gui.add(terrainMaterial.uniforms.mask2high, 'value', -1, 1).name('Mask2 High');
+}
+
+function mainLoop() {
+ requestAnimationFrame(mainLoop);
+ renderer.render(scene, camera);
+}
+
+function main() {
+ buildScene();
+ createMenu();
+ mainLoop();
+}
+
+setupThreeJs();
+loadTextures(main);
diff --git a/tp/src/standalone/tunnel.js b/tp/src/standalone/tunnel.js
@@ -0,0 +1,166 @@
+import * as THREE from 'three';
+import * as dat from 'dat.gui';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+
+let scene, camera, renderer, container;
+
+const textures = {
+ madera: { url: '/assets/madera.jpg', object: null },
+};
+
+function onResize() {
+ camera.aspect = container.offsetWidth / container.offsetHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(container.offsetWidth, container.offsetHeight);
+}
+
+function setupThreeJs() {
+ scene = new THREE.Scene();
+ container = document.getElementById('mainContainer');
+
+ renderer = new THREE.WebGLRenderer();
+ renderer.setClearColor(0x606060);
+ container.appendChild(renderer.domElement);
+
+ camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000);
+ camera.position.set(-50, 60, 50);
+ camera.lookAt(0, 0, 0);
+
+ const controls = new OrbitControls(camera, renderer.domElement);
+
+ const ambientLight = new THREE.AmbientLight(0xffffff);
+ scene.add(ambientLight);
+
+ const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.25);
+ scene.add(hemisphereLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(100, 100, 100);
+ scene.add(directionalLight);
+
+ const gridHelper = new THREE.GridHelper(50, 20);
+ scene.add(gridHelper);
+
+ const axesHelper = new THREE.AxesHelper(5);
+ scene.add(axesHelper);
+
+ window.addEventListener('resize', onResize);
+ onResize();
+}
+
+function buildScene() {
+ console.log('Building scene');
+}
+
+function onTextureLoaded(key, texture) {
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+ textures[key].object = texture;
+ console.log('Texture `' + key + '` loaded');
+}
+
+function loadTextures(callback) {
+ const loadingManager = new THREE.LoadingManager();
+
+ loadingManager.onLoad = () => {
+ console.log('All textures loaded');
+ callback();
+ };
+
+ for (const key in textures) {
+ console.log("Loading textures");
+ const loader = new THREE.TextureLoader(loadingManager);
+ const texture = textures[key];
+ texture.object = loader.load(
+ texture.url,
+ onTextureLoaded.bind(this, key),
+ null,
+ (error) => {
+ console.error(error);
+ }
+ );
+ }
+}
+
+function generateTunnel() {
+ const tunnelHeight = 20;
+ const tunnelWidth = 14;
+ const tunnelWallThickness = 0.5;
+ const tunnelLen = 26;
+
+ const path = new THREE.Path();
+ path.moveTo(-tunnelWidth/2, 0);
+ path.lineTo(-tunnelWidth/2, tunnelHeight*1/3);
+ path.moveTo(-tunnelWidth/2, tunnelHeight*1/3);
+ path.quadraticCurveTo(0, tunnelHeight, tunnelWidth/2, tunnelHeight*1/3);
+ path.moveTo(tunnelWidth/2, 0);
+ path.lineTo(tunnelWidth/2, 0);
+
+
+ // cerramos la curva con otra de la misma forma con una diferencia de
+ // `tunnelWallThickness`
+ path.lineTo(tunnelWidth/2-tunnelWallThickness, 0);
+ path.moveTo(tunnelWidth/2-tunnelWallThickness, 0);
+
+ path.lineTo(tunnelWidth/2-tunnelWallThickness, tunnelHeight*1/3);
+ path.moveTo(tunnelWidth/2-tunnelWallThickness, tunnelHeight*1/3);
+
+ path.quadraticCurveTo(
+ 0, tunnelHeight-(tunnelWallThickness*2),
+ -tunnelWidth/2+tunnelWallThickness, tunnelHeight*1/3);
+
+ path.lineTo(-tunnelWidth/2+tunnelWallThickness, 0);
+ path.moveTo(-tunnelWidth/2+tunnelWallThickness, 0);
+
+ path.lineTo(-tunnelWidth/2, 0);
+ path.moveTo(-tunnelWidth/2, 0);
+
+ const points = path.getPoints();
+
+ /*
+ // muestra la curva utilizada para la extrusión
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
+ const lineMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
+ const curveObject = new THREE.Line(geometry, lineMaterial);
+ scene.add(curveObject);
+ */
+
+ const shape = new THREE.Shape(points);
+
+ const extrudeSettings = {
+ curveSegments: 24,
+ steps: 50,
+ depth: tunnelLen,
+ };
+
+ const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
+ geometry.translate(0, 0, -tunnelLen/2);
+
+ textures.madera.object.wrapS = THREE.RepeatWrapping;
+ textures.madera.object.wrapT = THREE.RepeatWrapping;
+ textures.madera.object.repeat.set(0.10, 0.10);
+ textures.madera.object.anisotropy = 16;
+
+ const tunnelMaterial = new THREE.MeshPhongMaterial({
+ side: THREE.DoubleSide,
+ transparent: false,
+ opacity: 1.0,
+ shininess: 10,
+ map: textures.madera.object
+ });
+
+ const mesh = new THREE.Mesh(geometry, tunnelMaterial) ;
+ scene.add(mesh);
+}
+
+function mainLoop() {
+ requestAnimationFrame(mainLoop);
+ renderer.render(scene, camera);
+}
+
+function main() {
+ generateTunnel();
+ mainLoop();
+}
+
+setupThreeJs();
+loadTextures(main);
diff --git a/tp/terrain.html b/tp/terrain.html
@@ -13,6 +13,6 @@
</head>
<body>
<div id="mainContainer"></div>
- <script type="module" src="/src/terrain.js"></script>
+ <script type="module" src="/src/standalone/terrain.js"></script>
</body>
</html>
diff --git a/tp/track-map.html b/tp/track-map.html
@@ -13,6 +13,6 @@
</head>
<body>
<div id="mainContainer"></div>
- <script type="module" src="/src/track-map.js"></script>
+ <script type="module" src="/src/standalone/track-map.js"></script>
</body>
</html>
diff --git a/tp/train.html b/tp/train.html
@@ -13,6 +13,6 @@
</head>
<body>
<div id="mainContainer"></div>
- <script type="module" src="/src/train.js"></script>
+ <script type="module" src="/src/standalone/train.js"></script>
</body>
</html>
diff --git a/tp/trees.html b/tp/trees.html
@@ -13,6 +13,6 @@
</head>
<body>
<div id="mainContainer"></div>
- <script type="module" src="/src/trees.js"></script>
+ <script type="module" src="/src/standalone/trees.js"></script>
</body>
</html>
diff --git a/tp/tunnel.html b/tp/tunnel.html
@@ -13,6 +13,6 @@
</head>
<body>
<div id="mainContainer"></div>
- <script type="module" src="/src/tunnel.js"></script>
+ <script type="module" src="/src/standalone/tunnel.js"></script>
</body>
</html>