TA159

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

add section for final scene

Diffstat:
Mtp/index.html | 8+++++++-
Atp/scene.html | 18++++++++++++++++++
Atp/src/scene.js | 420+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 445 insertions(+), 1 deletion(-)
diff --git a/tp/index.html b/tp/index.html
@@ -21,8 +21,10 @@
         </style>
     </head>
     <body>
-        <h1>Trabajo Practico: Escena 3D</h1>
+        <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>
@@ -33,5 +35,9 @@
             <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/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/scene.js b/tp/src/scene.js
@@ -0,0 +1,420 @@
+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;
+        }
+
+        position.x -= widthSegments/2;
+        position.y += amplitudeBottom;
+        position.z -= heightSegments/2;
+        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);
+    }
+
+    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);