Post

Esferas Geodésicas: Refinamento de Malhas Triangulares

Esferas Geodésicas: Refinamento de Malhas Triangulares

Contexto

Este post documenta a solução de um exercício da disciplina de verão Temas de Computação Visual - 2026 do IMPA, especificamente sobre Modelagem Geométrica. O objetivo é criar um modelo geométrico de uma esfera geodésica através do refinamento progressivo de malhas triangulares, utilizando sólidos platônicos como base.

O Problema

O exercício propõe criar um modelo geométrico no formato OBJ para uma esfera geodésica, obtida refinando faces triangulares usando como malha inicial um sólido platônico de faces triangulares (tetraedro, octaedro ou icosaedro).

Requisitos principais:

  • Após cada refinamento, projetar os vértices na esfera unitária
  • Utilizar uma tabela hash para unificar os vértices
  • Exportar o resultado em formato OBJ

Fundamentação Teórica

Esferas Geodésicas

Uma esfera geodésica é uma aproximação poliédrica de uma esfera construída através da subdivisão iterativa de um poliedro base. O processo gera uma malha triangular cada vez mais refinada que se aproxima de uma esfera perfeita.

Sólidos Platônicos como Base

Existem três sólidos platônicos compostos apenas por faces triangulares:

  • Tetraedro: 4 vértices, 6 arestas, 4 faces
  • Octaedro: 6 vértices, 12 arestas, 8 faces
  • Icosaedro: 12 vértices, 30 arestas, 20 faces

Para este exercício, escolhi o icosaedro como malha inicial, pois ele possui o maior número de faces triangulares, resultando em uma aproximação mais uniforme da esfera desde o início.

Processo de Refinamento

O refinamento de cada triângulo segue o esquema de subdivisão 1-para-4:

  1. Calcular o ponto médio de cada aresta do triângulo
  2. Projetar esses pontos médios na esfera unitária
  3. Conectar os pontos médios, criando 4 novos triângulos

Este processo quadruplica o número de faces a cada iteração:

  • Nível 0 (icosaedro): 20 faces
  • Nível 1: 80 faces
  • Nível 2: 320 faces
  • Nível 3: 1,280 faces
  • Nível $n$: $20 \times 4^n$ faces

Projeção na Esfera Unitária

Para garantir que todos os vértices permaneçam na superfície da esfera, após calcular cada ponto médio $\mathbf{p} = (x, y, z)$, aplicamos a normalização:

\[\mathbf{p}_{normalizado} = \frac{\mathbf{p}}{||\mathbf{p}||} = \frac{(x, y, z)}{\sqrt{x^2 + y^2 + z^2}}\]

Esta operação projeta o ponto radialmente sobre a esfera unitária.

Solução Implementada

Estrutura de Dados

O código utiliza três estruturas principais:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Vec3
{
    double x, y, z;
    
    Vec3 normalized() const
    {
        double len = std::sqrt(x*x + y*y + z*z);
        if (len > 0)
            return Vec3(x/len, y/len, z/len);
        return *this;
    }
    
    // Operador para usar como chave no map
    bool operator<(const Vec3& v) const
    {
        if (x != v.x) return x < v.x;
        if (y != v.y) return y < v.y;
        return z < v.z;
    }
};
1
2
3
4
5
struct Triangle
{
    int v1, v2, v3;
    Triangle(int a, int b, int c) : v1(a), v2(b), v3(c) {}
};

Unificação de Vértices com Hash Table

Um aspecto crucial da implementação é evitar a duplicação de vértices. Quando dois triângulos adjacentes são refinados, eles compartilham uma aresta e, consequentemente, devem compartilhar o mesmo ponto médio.

Para isso, utilizei um std::map como tabela hash:

1
2
3
4
5
6
std::map<std::pair<int, int>, int> midpointCache;

std::pair<int, int> makeKey(int a, int b)
{
    return (a < b) ? std::make_pair(a, b) : std::make_pair(b, a);
}

A função makeKey garante que a aresta entre os vértices $i$ e $j$ sempre gere a mesma chave, independentemente da ordem $(i,j)$ ou $(j,i)$.

Algoritmo de Refinamento

O método refine() implementa a subdivisão 1-para-4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void refine() {
    std::vector<Triangle> newFaces;
    midpointCache.clear();
    
    for (const auto& tri : faces) {
        // Obtém os pontos médios de cada aresta
        int a = getMiddlePoint(tri.v1, tri.v2);
        int b = getMiddlePoint(tri.v2, tri.v3);
        int c = getMiddlePoint(tri.v3, tri.v1);
        
        // Cria 4 novos triângulos
        newFaces.push_back(Triangle(tri.v1, a, c));
        newFaces.push_back(Triangle(tri.v2, b, a));
        newFaces.push_back(Triangle(tri.v3, c, b));
        newFaces.push_back(Triangle(a, b, c));
    }
    
    faces = newFaces;
}

A função getMiddlePoint calcula e cacheia os pontos médios:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int getMiddlePoint(int v1, int v2)
{
    auto key = makeKey(v1, v2);
    
    // Verifica se já existe no cache
    auto it = midpointCache.find(key);
    if (it != midpointCache.end()) {
        return it->second;
    }
    
    // Calcula o ponto médio
    Vec3 p1 = vertices[v1];
    Vec3 p2 = vertices[v2];
    Vec3 middle = (p1 + p2) * 0.5;
    
    // Projeta na esfera unitária
    middle = middle.normalized();
    
    // Adiciona aos vértices
    vertices.push_back(middle);
    int index = vertices.size() - 1;
    
    // Armazena no cache
    midpointCache[key] = index;
    
    return index;
}

Inicialização do Icosaedro

Os vértices do icosaedro foram fornecidos :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void initializeIcosahedron() {
    vertices = {
        Vec3(0, 0.30901699437495, -0.5),
        Vec3(0.30901699437495, 0.5, 0),
        Vec3(-0.30901699437495, 0.5, 0),
        Vec3(0, 0.30901699437495, 0.5),
        Vec3(0, -0.30901699437495, 0.5),
        Vec3(-0.5, 0, 0.30901699437495),
        Vec3(0.5, 0, 0.30901699437495),
        Vec3(0, -0.30901699437495, -0.5),
        Vec3(0.5, 0, -0.30901699437495),
        Vec3(-0.5, 0, -0.30901699437495),
        Vec3(0.30901699437495, -0.5, 0),
        Vec3(-0.30901699437495, -0.5, 0)
    };
    
    // Normaliza para esfera unitária
    for (auto& v : vertices) {
        v = v.normalized();
    }
    
    // 20 faces do icosaedro
    faces = {
        Triangle(0, 2, 1), Triangle(3, 1, 2), Triangle(3, 5, 4),
        Triangle(3, 4, 6), Triangle(0, 8, 7), Triangle(0, 7, 9),
        Triangle(4, 11, 10), Triangle(7, 10, 11), Triangle(2, 9, 5),
        Triangle(11, 5, 9), Triangle(1, 6, 8), Triangle(10, 8, 6),
        Triangle(3, 2, 5), Triangle(3, 6, 1), Triangle(0, 9, 2),
        Triangle(0, 1, 8), Triangle(7, 11, 9), Triangle(7, 8, 10),
        Triangle(4, 5, 11), Triangle(4, 10, 6)
    };
}

Exportação para OBJ

O formato OBJ é simples e amplamente suportado:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void exportOBJ(const std::string& filename) {
    std::ofstream file(filename);
    
    file << std::fixed << std::setprecision(8);
    
    // Escreve os vértices
    for (const auto& v : vertices) {
        file << "v " << v.x << " " << v.y << " " << v.z << "\n";
    }
    
    // Escreve as faces (OBJ usa índices começando em 1)
    for (const auto& f : faces) {
        file << "f " << (f.v1 + 1) << " " << (f.v2 + 1) 
             << " " << (f.v3 + 1) << "\n";
    }
    
    file.close();
}

Execução e Resultados

O programa aceita o número de subdivisões como argumento:

1
2
g++ -std=c++11 -o geodesic prime.cpp
./geodesic 3

Saída:

1
2
3
4
5
6
7
Gerando esfera geodésica com 3 subdivisões...
Refinamento 1...
Refinamento 2...
Refinamento 3...
Arquivo exportado: geodesic_sphere_3.obj
Vértices: 642
Faces: 1280

Icosaedro Original

Após 3 refinamentos

Progressão do Refinamento

NívelVérticesFacesArestas
0122030
14280120
2162320480
36421,2801,920
42,5625,1207,680
510,24220,48030,720
640,96281,920122,880

A relação de Euler para poliedros ($V - E + F = 2$) é mantida em todos os níveis, confirmando a integridade topológica da malha.

Análise de Complexidade

Complexidade Espacial

  • Vértices: $O(4^n)$ - cresce exponencialmente com o nível de refinamento
  • Faces: $O(4^n)$ - cada face se divide em 4
  • Cache: $O(E)$ onde $E$ é o número de arestas, proporcional ao número de faces

Complexidade Temporal

  • Por refinamento: $O(F)$ onde $F$ é o número de faces atual
  • Total para $n$ refinamentos: $O(\sum_{i=0}^{n-1} 4^i) = O(4^n)$

Código Completo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <cmath>
#include <iomanip>
#include <sstream>

struct Vec3
{
    double x, y, z;
    
    Vec3(double x = 0, double y = 0, double z = 0) : x(x), y(y), z(z) {}
    
    Vec3 operator+(const Vec3& v) const
    {
        return Vec3(x + v.x, y + v.y, z + v.z);
    }
    
    Vec3 operator*(double s) const
    {
        return Vec3(x * s, y * s, z * s);
    }
    
    double length() const
    {
        return std::sqrt(x*x + y*y + z*z);
    }
    
    Vec3 normalized() const
    {
        double len = length();
        if (len > 0)
        {
            return Vec3(x/len, y/len, z/len);
        }
        return *this;
    }
    
    bool operator<(const Vec3& v) const
    {
        if (x != v.x) return x < v.x;
        if (y != v.y) return y < v.y;
        return z < v.z;
    }
};

struct Triangle
{
    int v1, v2, v3;
    Triangle(int a, int b, int c) : v1(a), v2(b), v3(c) {}
};

class GeodesicSphere
{
private:
    std::vector<Vec3> vertices;
    std::vector<Triangle> faces;
    std::map<std::pair<int, int>, int> midpointCache;
    
    std::pair<int, int> makeKey(int a, int b)
    {
        return (a < b) ? std::make_pair(a, b) : std::make_pair(b, a);
    }
    
    int getMiddlePoint(int v1, int v2)
    {
        auto key = makeKey(v1, v2);
        
        auto it = midpointCache.find(key);
        if (it != midpointCache.end()) {
            return it->second;
        }
        
        Vec3 p1 = vertices[v1];
        Vec3 p2 = vertices[v2];
        Vec3 middle = (p1 + p2) * 0.5;
        
        middle = middle.normalized();
        
        vertices.push_back(middle);
        int index = vertices.size() - 1;
        
        midpointCache[key] = index;
        
        return index;
    }
    
public:
    void initializeIcosahedron()
    {
        vertices = {
            Vec3(0, 0.30901699437495, -0.5),
            Vec3(0.30901699437495, 0.5, 0),
            Vec3(-0.30901699437495, 0.5, 0),
            Vec3(0, 0.30901699437495, 0.5),
            Vec3(0, -0.30901699437495, 0.5),
            Vec3(-0.5, 0, 0.30901699437495),
            Vec3(0.5, 0, 0.30901699437495),
            Vec3(0, -0.30901699437495, -0.5),
            Vec3(0.5, 0, -0.30901699437495),
            Vec3(-0.5, 0, -0.30901699437495),
            Vec3(0.30901699437495, -0.5, 0),
            Vec3(-0.30901699437495, -0.5, 0)
        };
        
        for (auto& v : vertices)
        {
            v = v.normalized();
        }
        
        faces =
        {
            Triangle(0, 2, 1), Triangle(3, 1, 2), Triangle(3, 5, 4),
            Triangle(3, 4, 6), Triangle(0, 8, 7), Triangle(0, 7, 9),
            Triangle(4, 11, 10), Triangle(7, 10, 11), Triangle(2, 9, 5),
            Triangle(11, 5, 9), Triangle(1, 6, 8), Triangle(10, 8, 6),
            Triangle(3, 2, 5), Triangle(3, 6, 1), Triangle(0, 9, 2),
            Triangle(0, 1, 8), Triangle(7, 11, 9), Triangle(7, 8, 10),
            Triangle(4, 5, 11), Triangle(4, 10, 6)
        };
    }
    
    void refine()
    {
        std::vector<Triangle> newFaces;
        midpointCache.clear();
        
        for (const auto& tri : faces)
        {
            int a = getMiddlePoint(tri.v1, tri.v2);
            int b = getMiddlePoint(tri.v2, tri.v3);
            int c = getMiddlePoint(tri.v3, tri.v1);
            
            newFaces.push_back(Triangle(tri.v1, a, c));
            newFaces.push_back(Triangle(tri.v2, b, a));
            newFaces.push_back(Triangle(tri.v3, c, b));
            newFaces.push_back(Triangle(a, b, c));
        }
        
        faces = newFaces;
    }
    
    void exportOBJ(const std::string& filename) {
        std::ofstream file(filename);
        if (!file.is_open()) {
            std::cerr << "Erro ao abrir arquivo: " << filename << std::endl;
            return;
        }
        
        file << std::fixed << std::setprecision(8);
        
        for (const auto& v : vertices)
        {
            file << "v " << v.x << " " << v.y << " " << v.z << "\n";
        }
        
        for (const auto& f : faces)
        {
            file << "f " << (f.v1 + 1) << " " << (f.v2 + 1) << " " << (f.v3 + 1) << "\n";
        }
        
        file.close();
        std::cout << "Arquivo exportado: " << filename << std::endl;
        std::cout << "Vértices: " << vertices.size() << std::endl;
        std::cout << "Faces: " << faces.size() << std::endl;
    }
};

int main(int argc, char* argv[])
{
    int subdivisions = 2; 
    
    if (argc > 1) {
        subdivisions = std::atoi(argv[1]);
        if (subdivisions < 0 || subdivisions > 6)
        {
            std::cout << " deve estar entre 0 e 6" << std::endl;
            return 1;
        }
    }
    
    
    GeodesicSphere sphere;
    sphere.initializeIcosahedron();
    
    for (int i = 0; i < subdivisions; i++)
    {
        std::cout << "iteracao " << (i + 1)  << std::endl;
        sphere.refine();
    }
    
    std::stringstream filename;
    filename << "geodesic_sphere_" << subdivisions << ".obj";
    sphere.exportOBJ(filename.str());
    
    return 0;
}

Para compilar e executar:

1
2
g++ main.cpp -o esfera
./esfera 2  # Gera esfera com 2 subdivisões

Conclusão

A implementação de uma esfera geodésica através do refinamento de um icosaedro demonstra conceitos importantes de modelagem geométrica computacional, incluindo subdivisão de malhas, projeção em superfícies implícitas e otimização de estruturas de dados.

O uso de tabelas hash para unificação de vértices é essencial para manter a eficiência e a integridade topológica da malha. A progressão geométrica do número de faces ($4^n$) mostra a necessidade de cuidado ao escolher o nível de refinamento, equilibrando qualidade visual e uso de recursos computacionais.

O resultado é uma aproximação poliédrica de alta qualidade de uma esfera, útil em diversas aplicações como computação gráfica, simulações físicas, análise de elementos finitos e modelagem científica.

This post is licensed under CC BY 4.0 by the author.