Ritorna all'inizio


3d

La proiezione di un arbitrario oggetto 3d su schermo è semplice. Vediamo passo passo quali sono le fasi per renderizzare a schermo un oggetto vettoriale.

Sia dato un "canvas" di dimensioni width*height. Vogliamo rappresentare a schermo un oggetto, e pertato lo definiamo tramite un insieme di vertici di coordinate (x, y, z) e facce (una faccia è un insieme di vertici). Nell'esempio utilizzeremo la libreria SDL e il linguaggio C. Nel nostro caso la cosa è agevole:

typedef struct {

int v[4];

} Face;

typedef struct {

float x;

float y;

float z;

} Vec3;

typedef struct {

float x;

float y;

} Vec2;

const int NUM_VERTICES = 8;

Vec3 vs[8] = {

{ -0.5f, 0.5f, -0.5f },

{ 0.5f, 0.5f, -0.5f },

{ 0.5f, 0.5f, 0.5f },

{ -0.5f, 0.5f, 0.5f },

{ -0.5f, -0.5f, -0.5f },

{ 0.5f, -0.5f, -0.5f },

{ 0.5f, -0.5f, 0.5f },

{ -0.5f, -0.5f, 0.5f }

};

const int NUM_FACES = 6;

Face fs[6] = {

{ {0, 1, 2, 3} },

{ {7, 6, 5, 4} },

{ {3, 2, 6, 7} },

{ {1, 0, 4, 5} },

{ {2, 1, 5, 6} },

{ {0, 3, 7, 4} }

};

L'insieme di vertici vs e facce fs definisce un cubo. Ci si può convincere facilmente della correttezza di queste coordinate disegnandole in un quaderno e unendo i puntini (i vertici) usando lo schema dato dalle facce. Dunque il vertice 0 viene unito con il vertice 1, il vertice 1 viene unito al vertice 2, e così via per ogni faccia fino a formare sei quadrilateri.

Nella maggior parte dei framework di sviluppo il sistema di coordinate è una griglia che parte dalla coordinata (0, 0) corrispondente all'angolo in alto a sinistra. Il nostro sistema di coordinate "matematico", invece, considera (0, 0) il centro dello schermo. Dobbiamo dunque convertire il sistema di coordinate con una funzione che mappa le coordinate "reali" a quelle "ideali":

Vec2 screen(Vec2 p) {

Vec2 s;

s.x = (p.x + 1.0f) / 2.0f * width;

s.y = (1.0f - (p.y + 1.0f) / 2.0f) * height;

return s;

}

Questa funzione sarà applicata alla proiezione di un vertice v, che per l'appunto mappa le coordinate spaziali dei vertici in coordinate 2d da mostrare su schermo. Concentriamoci sulla proiezione prospettica.

La proiezione prospettica è estremamente semplice:

(x, y, z) -> x' = x/z, y' = y/z

Questo fatto deriva da un'osservazione geometrica: Il punto si trova "dietro" lo schermo che guardiamo, e quello che ci interessa è proprio il punto di intersezione del "raggio" proiettato dal nostro occhio all'oggetto con lo schermo. Osserviamo due triangoli: il primo è il triangolo che ha come ipotenusa la distanza che si frappone tra noi e l'oggetto (che si trova al punto p di coordinate (x,y,z), il secondo è la distanza tra noi e il punto di intersezione di questo raggio con lo schermo (punto p' di coordinate (x', y')). Notiamo che i due triangoli sono simili (hanno gli stessi angoli), e pertanto vale l'equivalenza:

1 x'

- = -

z x

Assumiamo la distanza tra osservatore e schermo normalizzata e pari a 1.

La formula inversa per trovare z è immediata. Analogo il discorso per la y.

Definiamo questo concetto in una funzione di proiezione prospettica:

Vec2 project_persp(Vec3 v) {

Vec2 p;

float z = v.z;

if (z == 0.0f) z = 0.00001f;

p.x = v.x / z;

p.y = v.y / z;

return p;

}

Dato che z può teoricamente essere 0, cioè l'osservatore e l'oggetto condividono la stessa posizione ("siamo dentro l'oggetto"), allora per evitare una divisione per zero lo "spostiamo" di una frazione più in avanti.

Questo è in realtà tutto ciò che ci serve per disegnare a schermo un oggetto di forma e complessità arbitrarie. Dato che stiamo di fatto costruendo un piccolo engine grafico 3d, vorremmo idealmente poter applicare delle trasformazioni (rotazioni, traslazioni ecc.) per poter osservare un oggetto in movimento.

Implementare delle funzioni di rotazione e traslazione è altrettanto facile.

La traslazione è essenzialmente l'equivalente di una addizione applicata alle componenti del vettore, in varia misura a seconda della trasformazione voluta.

Vec3 translate(Vec3 v, float dx, float dy, float dz) {

return (Vec3){ v.x + dx, v.y + dy, v.z + dz };

}

La rotazione può essere fatta sui tre assi, e segue la classica formula di Eulero

Vec3 rotate_xz(Vec3 v, float angle) {

float c = cosf(angle);

float s = sinf(angle);

return (Vec3){

v.x * c - v.z * s,

v.y,

v.x * s + v.z * c

};

}

Questa funzione ruota il vertice sul proprio asse delle y.

Vec3 rotate_yz(Vec3 v, float angle) {

float c = cosf(angle);

float s = sinf(angle);

return (Vec3){

v.x,

v.y * c - v.z * s,

v.y * s + v.z * c

};

}

Questa sull'asse delle x.

Applichiamo tutte le trasformazioni in una funzione.

Vec2 transform(Vec3 v, float angle, float dx, float dy, float dz) {

v = rotate_xz(v, angle);

v = translate(v, dx, dy, dz);

return screen(project_persp(v));

}

Per renderizzare l'oggetto dobbiamo disegnare delle linee da vertice a vertice, e lo facciamo tramite due cicli annidati:

for (int f_idx = 0; f_idx < NUM_FACES; ++f_idx) {

int v_count = fs[f_idx].count;

for (int i = 0; i < v_count; ++i) {

int v1_index = fs[f_idx].v[i];

int v2_index = fs[f_idx].v[(i + 1) % v_count];

Vec2 p1 = projectedVs[v1_index];

Vec2 p2 = projectedVs[v2_index];

SDL_RenderDrawLine(renderer, (int)p1.x, (int)p1.y, (int)p2.x, (int)p2.y);

}

}

Per ogni faccia colleghiamo il vertice al prossimo, l'ultimo con il primo.

Questo ciclo, a sua volta è eventualmente situato in un event loop in cui si possono incrementare dei valori a seconda di come si vuole modificare la configurazione dell'oggetto in ogni frame.

Il risultato finale sarà così:

#include <SDL2/SDL.h>

#include <stdio.h>

#include <math.h>

#define _USE_MATH_DEFINES

int width = 800;

int height = 800;

SDL_Renderer *renderer;

typedef struct {

int count;

int v[4];

} Face;

typedef struct {

float x;

float y;

float z;

} Vec3;

typedef struct {

float x;

float y;

} Vec2;

const int NUM_VERTICES = 8;

Vec3 vs[8] = {

{ -0.5f, 0.5f, -0.5f },

{ 0.5f, 0.5f, -0.5f },

{ 0.5f, 0.5f, 0.5f },

{ -0.5f, 0.5f, 0.5f },

{ -0.5f, -0.5f, -0.5f },

{ 0.5f, -0.5f, -0.5f },

{ 0.5f, -0.5f, 0.5f },

{ -0.5f, -0.5f, 0.5f }

};

const int NUM_FACES = 6;

Face fs[6] = {

{ 4, {0, 1, 2, 3} },

{ 4, {7, 6, 5, 4} },

{ 4, {3, 2, 6, 7} },

{ 4, {1, 0, 4, 5} },

{ 4, {2, 1, 5, 6} },

{ 4, {0, 3, 7, 4} }

};

Vec2 project_persp(Vec3 v) {

Vec2 p;

float z = v.z;

if (z == 0.0f) z = 0.00001f;

p.x = v.x / z;

p.y = v.y / z;

return p;

}

Vec2 project_iso(Vec3 v) {

Vec2 p;

float cos30 = 0.866025f;

float sin30 = 0.5f;

p.x = (v.x - v.z) * cos30;

p.y = v.y - (v.x + v.z) * sin30;

return p;

}

Vec2 screen(Vec2 p) {

Vec2 s;

s.x = (p.x + 1.0f) / 2.0f * width;

s.y = (1.0f - (p.y + 1.0f) / 2.0f) * height;

return s;

}

Vec3 translate(Vec3 v, float dx, float dy, float dz) {

return (Vec3){ v.x + dx, v.y + dy, v.z + dz };

}

Vec3 rotate_xz(Vec3 v, float angle) {

float c = cosf(angle);

float s = sinf(angle);

return (Vec3){

v.x * c - v.z * s,

v.y,

v.x * s + v.z * c

};

}

Vec3 rotate_yz(Vec3 v, float angle) {

float c = cosf(angle);

float s = sinf(angle);

return (Vec3){

v.x,

v.y * c - v.z * s,

v.y * s + v.z * c

};

}

Vec2 transform(Vec3 v, float angle, float dx, float dy, float dz) {

v = rotate_xz(v, angle);

v = translate(v, dx, dy, dz);

return screen(project_persp(v));

}

int main() {

if (SDL_Init(SDL_INIT_VIDEO) != 0) {

printf("SDL could not be initialized! SDL_Error: %s\n", SDL_GetError());

return 1;

}

SDL_Window *window = SDL_CreateWindow(

"3D Engine",

SDL_WINDOWPOS_CENTERED,

SDL_WINDOWPOS_CENTERED,

width,

height,

SDL_WINDOW_SHOWN

);

if (window == NULL) {

printf("Window could not be created! SDL_Error: %s\n", SDL_GetError());

SDL_Quit();

return 1;

}

renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

if (renderer == NULL) {

printf("Renderer could not be created! SDL_Error: %s\n", SDL_GetError());

SDL_DestroyWindow(window);

SDL_Quit();

return 1;

}

int running = 1;

SDL_Event event;

Vec2 projectedVs[NUM_VERTICES];

float dx = 0.0f, dy = 0.0f, dz = 1.5f;

float angle = 0.0f;

float xp = 1.0f, yp = 1.0f, zp = 1.0f;

float dt = 1.0f / 180.0f;

// in loop

while (running) {

while (SDL_PollEvent(&event)) {

if (event.type == SDL_QUIT) running = 0;

}

angle += dt * M_PI;

if (dx - vs[0].x <= -1.0f || dx + vs[0].x >= 1.0f) xp *= -1.0f;

if (dy - vs[0].y <= -1.0f || dy + vs[0].y >= 1.0f) yp *= -1.0f;

//dx += dt * xp;

//dy += dt * yp;

//dz += dt * zp;

for (int i = 0; i < NUM_VERTICES; i++) {

projectedVs[i] = transform(vs[i], angle, dx, dy, dz);

}

SDL_SetRenderDrawColor(renderer, 16, 16, 16, 255);

SDL_RenderClear(renderer);

SDL_SetRenderDrawColor(renderer, 80, 255, 80, 255);

for (int f_idx = 0; f_idx < NUM_FACES; ++f_idx) {

int v_count = fs[f_idx].count;

for (int i = 0; i < v_count; ++i) {

int v1_index = fs[f_idx].v[i];

int v2_index = fs[f_idx].v[(i + 1) % v_count];

Vec2 p1 = projectedVs[v1_index];

Vec2 p2 = projectedVs[v2_index];

SDL_RenderDrawLine(renderer, (int)p1.x, (int)p1.y, (int)p2.x, (int)p2.y);

}

}

SDL_RenderPresent(renderer);

SDL_Delay(1000 / 180);

}

SDL_DestroyRenderer(renderer);

SDL_DestroyWindow(window);

SDL_Quit();

return 0;

}

Articoli correlati