Tema 2 – Pointers in 3D

Termen de predare

  • Deadline hard : Duminica 22 decembrie 2024, ora 23:55

Responsabili

  • Bogdan Butnariu
  • Cosmin-Ștefan Popa

Changelog

  • 12 decembrie 2024, 23:00 – Înlăturat warningul de folosit enum în loc de macro
  • 10 decembrie 2024, 15:35 – Mutat deadline soft și hard pe 22 decembrie 2024.
  • 3 decembrie 2024, 15:44 – Menționat în cerință despre fișierul README
  • 3 decembrie 2024, 15:28 – Adăugat flagul de linker -lm în Makefile
  • 1 decembrie 2024, 18:20 – Reparat instalarea modulelor de python din scriptul de setup
  • 1 decembrie 2024 – Reparat instalarea modulului venv din scriptul de setup
  • 1 decembrie 2024 – Întregit scriptul de setup din schelet

Întrebări

Dacă aveți nelămuriri, puteți să ne contactați pe forumul dedicat temei 2. Nu se acceptă întrebări în ultimele 24 de ore înainte de deadline.

Citiți cu atenție tot enunțul temei.

Intro

Minecraft e un joc video open-world în care jucătorii pot ridica construcții într-o lume formată din blocuri (cuburi) fixate într-o grilă tridimensională.

Fiindcă Minecraft este din nou în vogă, un arhitect și-a propus să construiască în joc clădirile pe care le proiectează și vă solicită ajutorul: are nevoie de un program care generează automat structuri arhitectonice grandioase.

Numim chunk o matrice tridimensională de blocuri (reprezentate de tipul char), fiecare bloc având trei coordonate:

  • x (de la 0 la width-1)
  • y (de la 0 la height-1)
  • z (de la 0 la depth-1)

Un exemplu de chunk este ilustrat în imagine:

Un chunk este reprezentat printr-un vector cu width elemente. Fiecare element chunk[x] reprezintă un plan paralel cu planul $yOz$, și este pointer la un vector cu height elemente. Mai departe, chunk[x][y] reprezintă un rând – din planul chunk[x] – paralel cu axa $Oz$, și este pointer la un vector cu depth elemente. În sfârșit, chunk[x][y][z] descrie blocul aflat la coordonatele $(x, y, z)$, conform următoarei encodări pentru care avem constante simbolice:

  • 0 = aer (BLOCK_AIR)
  • 1 = iarbă (BLOCK_GRASS)
  • 2 = lemn (BLOCK_WOOD)
  • 3 = piatră (BLOCK_STONE)

Toate funcțiile din fișierele chunk_gen.c, chunk_transform.c și chunk_process.c primesc ca parametru un chunk care se presupune că a fost deja alocat (dinamic) și returnează chunkul modificat. Dacă aplicați procesările direct pe chunkul primit ca parametru, este suficient să îl returnați tot pe el. Însă dacă alocați un nou chunk și aplicați procesările pe chunkul nou creat, chunkul original primit ca parametru trebuie dezalocat în aceeași funcție!

Atenție: pentru funcțiile care necesită schimbarea dimensiunii chunkului (e.g. rotate), este obligatoriu ca la finalul funcției să obținem un chunk cu exact atâta memorie alocată cât este necesar.

Schelet de cod

Pentru această temă trebuie să porniți de la scheletul de cod de aici: tema2-schelet-v7.zip.

Scheletul include instrumente utile pentru a compila și valida tema. În fișierul USAGE.md regăsiți instrucțiunile de utilizare a instrumentelor.

Pentru a rezolva tema, trebuie să completați funcțiile definite în fișierele din directorul libchunk.

Atenție! Nu aveți voie să:

  • redenumiți fișierele temei
  • modificați parametrii funcțiilor din schelet
  • modificați regulile din fișierul Makefile

Partea A - Generare de structuri

Pentru Partea A, veți implementa funcțiile definite în fișierul libchunk/chunk_gen.c.

Task 1 (4p) - Adăugare Bloc

Implementați funcția chunk_place_block care primește ca parametri un chunk de dimensiuni width, height, și depth, trei coordonate și un tip de bloc.

char*** chunk_place_block(
    char*** chunk, int width, int height, int depth,
    int x, int y, int z, char block);

Funcția întoarce chunkul obținut prin amplasarea blocului precizat la coordonatele primite ca parametru.

Atenție! Coordonatele pot depăși chunkul. Spre exemplu, coordonata x poate fi în afara intervalului [0, width-1]. Blocul se introduce numai dacă x, y și z se încadrează în limitele chunkului, altfel chunkul rămâne neschimbat.

Exemplu

Task 2 (6p) - Umplere Paralelipiped

Implementați funcția chunk_fill_cuboid:

char*** chunk_fill_cuboid(
    char*** chunk, int width, int height, int depth,
    int x0, int y0, int z0, int x1, int y1, int z1, char block);

Funcția primește coordonatele a două puncte – $(x_0, y_0, z_0)$ și $(x_1, y_1, z_1)$ – ce reprezintă colțuri opuse ale unui paralelipiped dreptunghic și un tip de bloc, și întoarce chunkul obținut prin umplerea paralelipipedului cu blocuri de tipul precizat.

Atenție! Și această funcție poate primi coordonate ce depășesc limitele chunkului. Numai partea din paralelipiped care se încadrează în dimensiunile chunkului va fi umplută.

În rezolvarea oricărui task, puteți folosi funcțiile pe care le-ați implementat deja.

Exemplu

Task 3 (6p) - Umplere sferă

Implementați funcția chunk_fill_sphere care introduce un bloc de un anumit tip la toate pozițiile aflate strict în interiorul sferei de centru și rază primite ca parametru. Funcția trebuie să întoarcă noul chunk.

char*** chunk_fill_sphere(
    char*** chunk, int width, int height, int depth,
    int x, int y, int z, double radius, char block);

Spre exemplu, dacă sfera are centru $(1, 1, 2)$ și rază $1.2$, atunci blocul de la $(1, 1, 3)$ va fi modificat de funcție, având distanța $1$ de centrul sferei. Însă blocul de la $(0, 0, 2)$ va rămâne intact, fiindcă stă la distanța $\sqrt{2} > 1.2$ de centrul sferei.

Atenție! La fel ca funcțiile anterioare, această funcție introduce blocuri numai la pozițiile aflate în limitele chunkului. Sfera descrisă de parametri nu se va încadra întotdeauna în dimensiunile chunkului.

Exemplu

Partea B - Procesare

Pentru a rezolva Partea B veți completa funcțiile din fișierul libchunk/chunk_process.c.

Task 4 (6p) - Înveliș în jurul unui bloc

Trebuie implementată funcția chunk_shell:

char*** chunk_shell(
    char*** chunk, int width, int height, int depth,
    char target_block, char shell_block);

În jurul fiecărui bloc de tipul target_block, funcția construiește un înveliș de tipul shell_block. Blocurile de tip target_block nu se înlocuiesc cu shell_block, însă celelalte blocuri se pot înlocui, dacă este cazul.

Învelișul unui bloc e format din pozițiile vecine directe — spre exemplu, $(x+1, y, z)$ sau $(x, y-1, z)$ — și pozițiile vecine de “muchie”, precum $(x+1, y, z+1)$ sau $(x, y-1, z-1)$, dar nu și pozițiile vecine de “colț”, precum $(x+1, y+1, z+1)$.

Atenție! La fel ca funcțiile anterioare, această funcție introduce blocuri numai la pozițiile aflate în interiorul chunkului.

Exemplu

Task 5 (8p) - Umplere în planul $xOz$

Veți implementa funcția chunk_fill_xz:

char*** chunk_fill_xz(
    char*** chunk, int width, int height, int depth,
    int x, int y, int z, char block);

Dacă la poziția $(x, y, z)$ se află un bloc de tip $B$, atunci funcția amplasează block pe toate pozițiile din planul paralel cu $xOz$ la care se poate ajunge prin deplasare cu câte un bloc, paralel cu axele $Ox$ și $Oz$, începând de la poziția $(x, y, z)$ și trecând numai prin blocuri de tip $B$.

Exemplu

Task 6 (10p) - Umplere în spațiu

Trebuie implementată funcția chunk_fill având același prototip ca chunk_fill_xz:

char*** chunk_fill(
    char*** chunk, int width, int height, int depth,
    int x, int y, int z, char block);

Funcția amplasează block pe pozițiile din spațiu la care se poate ajunge prin deplasare cu câte un bloc pe orice axă, pornind de la $(x, y, z)$, trecând numai prin blocuri identice cu cel de pe poziția $(x, y, z)$.

Exemplu

Partea C - Transformări

Pentru Partea C, veți lucra în fișierul libchunk/chunk_transform.c.

Task 7 (8p) - Rotire în jurul axei $Oy$

Veți implementa funcția chunk_rotate_y, care primește un chunk și întoarce chunkul rotit la 90 de grade în jurul axei $y$ (sau în planul $xOz$). Privită de sus, rotația se realizează în sens trigonometric.

char*** chunk_rotate_y(
    char*** chunk, int width, int height, int depth);

Atenție! Rotirea poate produce un chunk cu dimensiuni diferite de cel original. Dimensiunile chunkului returnat de funcție sunt width_nou = depth, height_nou = height și depth_nou = width.

Exemplu

Task 8 (12p) - Gravitație

Pentru acest task, aveți de implementat funcția chunk_apply_gravity:

char*** chunk_apply_gravity(
    char*** chunk, int width, int height, int depth, int* new_height);

Numim un corp o mulțime de blocuri de același fel (dar diferite de aer), conectate, în spațiu. Cu alte cuvinte, dacă aplicăm funcția chunk_fill cu coordonatele unui bloc (diferit de aer), se vor umple exact blocurile corespunzătoare corpului din care face parte blocul.

Funcția chunk_apply_gravity are ca efect coborârea tuturor corpurilor prezente în chunk, până când niciun corp nu se mai poate deplasa în jos — fie pentru că atinge baza chunkului ($y=0$), fie pentru că alt corp se află imediat dedesubt.

După această transformare, pot rămâne spații goale (aer) de forma unor plane paralele cu $xOz$ în partea de sus a chunkului. Aceste spații goale vor fi eliminate. Spre exemplu, dacă cele două plane paralele cu $xOz$ situate cel mai sus pe axa $y$ sunt alcătuite doar din blocuri de aer, chunkul întors nu le va include, și va avea new_height = height - 2 care trebuie transmis apelantului prin parametrul de ieșire new_height.

Exemplu

În exemplul de mai jos există șapte corpuri (patru cuburi, două inele și un baston). După aplicarea gravitației, cele 7 plane superioare paralele cu $xOz$ se vor elimina fiindcă rămân neocupate cu corpuri.

Partea D - Compression

Pentru Partea D veți completa cele două funcții din fișierul libchunk/chunk_compress.c.

Task 9 (10p) - Stocarea unui chunk

În reprezentarea cu matrici tridimensionale, chunkurile largi ocupă cantități mari de memorie. Un chunk de dimensiuni $20,20,20$ necesită memorarea tipului de bloc pentru 8000 de blocuri.

Putem reduce consumul de memorie observând că blocurile alăturate sunt adesea identice. În loc să stocăm același bloc de multe ori, vom păstra tipul de bloc și numărul de apariții consecutive, conform schemei de lossless compression numite run-length encoding (RLE).

Pentru început, vom aplatiza (flatten) matricea tridimensională numerotând blocurile de la 0 la width*height*depth - 1 în funcție de poziția fiecăruia, în ordinea $y,z,x$. Un exemplu este ilustrat mai jos (am separat straturile pentru claritate).

Parcurgând blocurile în ordinea precizată numărăm aparițiile consecutive (runs) ale aceluiași bloc. În exemplul anterior, există 5 runs: $(\textrm{iarba}, 4)$, $(\textrm{piatra}, 5)$, $(\textrm{aer}, 8)$, $(\textrm{iarba}, 1)$, $(\textrm{aer}, 9)$.

Dorim să memorăm într-un vector de unsigned char (fiecare unsigned char are 8 biți) informațiile despre aceste runs, una după alta, pentru a putea ulterior să reconstruim chunkul.

Dacă $\textrm{bloc}$ e unul dintre cele patru blocuri posibile, atunci îi corespunde un număr de la 0 la 3, deci poate fi reprezentat de doi biți $b_1b_0$ (cifrele numărului corespunzător în baza doi). De asemenea, numărul de apariții $n$ dintr-un run, în baza 2, poate fi reprezentat de $q$ biți. Așadar memorăm fiecare run de forma $(\textrm{bloc}, n)$, individual, în următorul format, unde bb sunt biții ce specifică blocul iar n...nn biții ce specifică numărul de apariții:

Valoare $n$ Primul unsigned char Al doilea unsigned char
$1 \le n \lt 32 \implies q=5$ bb0nnnnn nu există
$32 \le n \lt 4096 \implies q=12$ bb10nnnn nnnnnnnn

În cazul în care $32 \le n \lt 4096$, cei mai semnificativi 4 biți din $n$ sunt stocați în primul unsigned char, iar ceilalți biți în al doilea. Dacă un run are $n \ge 4096$, se împarte într-un prim run cu $n_1 = 4095$ și un al doilea run cu $n_2 = n - 4095$, iar procesul se repetă.

Spre exemplu, dacă un chunk este descris de trei runs, $(\textrm{piatra}, 6)$, $(\textrm{lemn}, 501)$ și $(\textrm{iarba}, 5)$, îl reprezentăm prin vectorul de patru unsigned char:

În loc să memorăm 512 blocuri individual, am descris întregul chunk în 4 octeți!

Un chunk descris de un singur run $(\textrm{lemn}, 8192)$ îl reprezentăm prin vectorul de cinci unsigned char:

Pentru acest task trebuie să implementați funcția chunk_encode:

unsigned char* chunk_encode(
    char*** chunk, int width, int height, int depth,
    int* length);

Funcția primește un chunk și întoarce un vector de unsigned char ce reprezintă un șir de runs conform descrierii de mai sus. Prin length furnizează lungimea vectorului (numărul de unsigned chars).

Task 10 (10p) - Recuperarea unui chunk

Odată encodate cu RLE, chunkurile ocupă mai puțină memorie, însă dacă vrem să le vizualizăm sau să aplicăm operații asupra lor, trebuie convertite înapoi în forma matricială.

Funcția de implementat, chunk_decode, primește printr-un un vector de unsigned char un chunk encodat conform schemei de la Task 9 și dimensiunile chunkului. Funcția întoarce chunkul original sub forma unei matrici tridimensionale alocate dinamic, în formatul din introducerea temei.

char*** chunk_decode(
    unsigned char* code, int width, int height, int depth);

Task 11 (10p) - Valgrind

Pentru acest task, trebuie să aveți punctajul maxim pe taskurile 7, 8, 9 și 10 și să nu aveți scurgeri de memorie la verificarea lor folosind valgrind.

Se va rula valgrind folosind următoarea comandă:

valgrind --tool=memcheck --leak-check=full --error-exitcode=1 <program>

Pentru depanarea problemelor de memorie puteți apela în src/main.c funcțiile implementate și executa valgrind cu comanda de mai sus asupra programului build/main.

Notare

Coding style

Există o depunctare de până la -20p pentru coding style inadecvat. Checkerul verifică automat coding style-ul.

Validare locală

Checkerul local se regăsește în scheletul temei. Consultați fișierul USAGE.md pentru instrucțiunile de utilizare.

Trimitere

Tema va fi trimisă prin Moodle, cursul Programarea Calculatoarelor (CB & CD), activitatea Tema 2.

Toate temele sunt testate în mod automat pe Moodle.

Arhiva temei se va încărca prin formularul de submisie (butonul Add submission).

Rezultatele vor fi disponibile în secțiunea Feedback – nota apare la linia Grade, iar outputul checkerului și erorile apar la sectiunea Feedback comments. Dacă apare un buton albastru în formă de plus, trebuie să dați click pe el pentru a afișa întregul output al checkerului.

Citiți cu atenție informațiile afișate în Feedback pentru a vă asigura că tema a fost rulată cu succes. Asigurați-vă că conținutul arhivei respectă structura dorită (ex. fișierele sunt în directorul corect).

Conținutul arhivei trebuie să fie următorul:

  • directorul libchunk/
  • fișierul Makefile
  • un fișier README în care descrieți rezolvarea temei pe scurt

Arhiva trebuie să fie în format ZIP.

Nu includeți fișierele checkerului în arhivă.

Depunctări

Lista nu este exhaustivă.

  • O temă care nu compilează și nu a rulat pe VMChecker nu va fi luată în considerare.
  • O temă care nu rezolvă cerința și trece testele prin alte mijloace nu va fi luată în considerare.
  • Nu acceptăm teme copiate. În cazul unei teme copiate se scade punctajul aferent temei din punctajul total.
  • (-20p) Nerezolvarea tuturor erorilor și warningurilor de coding style.

Minecraft is a trademark of Mojang Studios. This page contains copyrighted materials owned by Mojang Studios. All rights reserved by Mojang Studios. These materials are used under fair use for educational purposes. This assignment is not affiliated with, endorsed by, or associated with Mojang Studios or Microsoft.

programare/teme_2024/tema2_2024_cbd.txt · Last modified: 2024/12/12 23:02 by cosmin_stefan.popa
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0