Mudanças entre as edições de "Perspectiva ou Projeção Cônica"

De Aulas
 
(5 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
Linha 1: Linha 1:
 
 
Afluentes: [[Computação Gráfica]]
 
Afluentes: [[Computação Gráfica]]
  
 
= Introdução =
 
= Introdução =
  
Para representarmos um espaço tridimensional, da forma como é vista pelo olho humano, em um plano bidimensional, no caso a tela do computador, é utilizada uma das mais importantes descobertas no mundo das artes, da qual introduziu o realismo nas pinturas e desenhos.
+
Para representarmos um espaço tridimensional — tal como percebido pelos olhos humanos — em um plano bidimensional, como a tela do computador, recorremos a uma das mais importantes descobertas do mundo das artes: a perspectiva.
 
 
A partir de análises visuais, a Perspectiva foi descoberta na busca de soluções geométricas para a construção da cúpula da Catedral de Florença pelo arquiteto italiano Brunelleschi (1377-1446).
 
  
A projeção perspectiva, produz uma imagem realista, porém não em suas verdadeiras medidas, executando uma operação dentro do espaço tridimensional para representar a cena vista de um ponto de observação a uma distância finita.
+
Desenvolvida a partir de análises visuais e necessidades construtivas, a projeção em perspectiva surge durante o Renascimento, na busca por soluções geométricas para a construção da cúpula da Catedral de Florença, pelo arquiteto italiano Filippo Brunelleschi (1377–1446).
  
Na projeção perspectiva, as coordenadas dos pontos projetados são obtidas pela intersecção dos raios projetores com o plano de projeção, conforme visto na Figura 1.
+
Essa técnica introduziu o realismo na pintura e no desenho, oferecendo uma maneira sistemática de representar a profundidade — ou melhor, a ilusão dela — sobre superfícies planas.
  
<center>[[Image:Perspectiva.png]]</center>
+
A projeção perspectiva produz uma imagem visualmente realista, embora não preserve as medidas reais dos objetos, pois executa uma transformação dentro do espaço tridimensional para representar a cena a partir de um ponto de observação a uma distância finita.
<center><small><b>Figura 1: Representação da Visualização em Perspectiva.</b></small></center>
 
  
 +
Na projeção perspectiva, as coordenadas dos pontos projetados são obtidas pela intersecção dos raios projetores com o plano de projeção, conforme ilustrado na Figura 1.<center>[[Arquivo:Perspectiva.png|nenhum|miniaturadaimagem|500x500px|<small><b>Figura 1: Representação da Visualização em Perspectiva.</b></small>]]</center>
 +
== Termos ==
 
Sendo que:
 
Sendo que:
* '''f''' é a distância do observador até o plano de projeção;
+
* '''(x, y, z)''': coordenadas de um ponto no espaço 3D (a cena que está sendo projetada).
 +
* f: a distância focal, que representa o quão longe está o plano da imagem do centro da projeção (o "olho" da câmera).
 +
* '''(x′,y′):''' coordenadas do ponto '''projetado no plano da imagem''', ou seja, no plano 2D.
 
* '''z''' é a distância do observador até o ponto original;
 
* '''z''' é a distância do observador até o ponto original;
* '''(x,y,z)''' é a localização espacial do ponto original;
 
* '''(x',y')''' é a representação do ponto original no plano de projeção, observando-se que o z não existe no plano de projeção, passando a ser z = 0;
 
  
 
Ao aplicarmos o ponto do objeto no plano da imagem, iremos criar o ponto de projeção. Dessa forma, estamos aplicando uma transformação do espaço tridimensional para o espaço bidimensional.
 
Ao aplicarmos o ponto do objeto no plano da imagem, iremos criar o ponto de projeção. Dessa forma, estamos aplicando uma transformação do espaço tridimensional para o espaço bidimensional.
  
Simplificando todo processo para se chegar à representação do ponto (x, y) no plano de projeção (x', y'), temos as seguintes fórmulas:
+
== Interpretação ==
 +
Imagine que a câmera está olhando na direção do eixo Z, com o plano de projeção paralelo ao plano XY, colocado a uma distância f do centro (em z = f). O ponto 3D é então projetado em linha reta até esse plano.
 +
 
 +
Dessa forma, temos que x' dividido por x é igual a f dividido por x que implica em x' é igual a f dividido por z, multiplicado por x.
 +
 
 +
<math>{ x' \over x } = { f \over z } \Rightarrow x' = { f \over z }  x</math>
 +
 
 +
Logo, para encontrar o x' que é a representação no plano do x do espaço 3D, usamos a fórmula x' = f dividido por z, multiplicado por x:
 +
 
 +
<math>x' = { f \over z } x</math>
 +
 
 +
E o mesmo se aplica a y ou seja, y' é igual a f dividido por z, multiplicado por y:
 +
 
 +
<math>y' = { f \over z }  y</math>
 +
 
 +
O '''z''' aqui é crucial — ele representa a profundidade. Quanto mais longe o objeto estiver (maior z), menor ele aparecerá na imagem — exatamente como nossos olhos percebem o mundo. Uma verdadeira dança de escala baseada na profundidade.
 +
 
 +
=== Formalizando mais... ===
 +
Podemos ver essa projeção como uma '''transformação projetiva''' que leva um ponto 3D para 2D, ignorando a componente de profundidade na imagem. Em coordenadas homogêneas:
 +
 
 +
<math>\begin{bmatrix}
 +
x' \\
 +
y' \\
 +
1
 +
\end{bmatrix}
 +
=
 +
\begin{bmatrix}
 +
f & 0 & 0 & 0 \\
 +
0 & f & 0 & 0 \\
 +
0 & 0 & 1 & 0
 +
\end{bmatrix}
 +
\cdot
 +
\begin{bmatrix}
 +
x \\
 +
y \\
 +
z \\
 +
1
 +
\end{bmatrix}
 +
\quad \Rightarrow \quad
 +
x' = \frac{f}{z}x, \quad y' = \frac{f}{z}y</math>
  
<center>[[Image:Perspectiva_formula_x1.png]]</center>
+
Para aplicarmos, fazemos uma operação de matrizes. Temos que a matriz vertical de três linhas e uma coluna, contendo x', y' e 1 é igual a matriz de três por quatro contendo na primeira linha: f, 0, 0 e 0, na segunda linha: 0, f, 0 e 0 e na terceira linha: 0, 0, 1 e 0, multiplicado pela matriz vertical de quatro por um contendo z, y, z e 1 da imagem 3D. Isso implica nas fórmulas para gerar x' e y' apresentadas anteriormente.
<center><small><b>Fórmula 1: Encontra o x do ponto de projeção.</b></small></center>
 
  
<center>[[Image:Perspectiva_formula_y1.png]]</center>
+
Essa abordagem é usada em gráficos computacionais, visão computacional, realidade aumentada, renderização 3D, etc.
<center><small><b>Fórmula 2: Encontra o y do ponto de projeção.</b></small></center>
 
  
Sendo assim, devemos criar uma nova cena distorcida no espaço euclidiano, mas suficiente para criar uma representação no plano de projeção. Dessa forma, vamos criar uma representação de cada elemento da cena em que para cada ponto do elemento vamos aplicar as fórmulas acima.
+
=== E para implementar? ===
 +
Tendo as equações já simplificadas, podemos agora aplicá-las na programação.
  
<pre>
+
O exemplo a seguir apresenta, em forma de algoritmo, a função de transformação do espaço euclidiano tridimensional para sua projeção cônica na tela do nosso dispositivo computacional.<syntaxhighlight lang="text">
 
Distancia = 300
 
Distancia = 300
 
 
Elemento2 = Elemento
 
Elemento2 = Elemento
  
Para i = 0 Até Quantidade_de_pontos Fazer
+
Para i = 0 até Quantidade_de_pontos fazer:
Elemento2[i].x = ( Distancia / Elemento[i].z ) * Elemento[i].x;
+
    Elemento2[i].x = (Distancia / Elemento[i].z) * Elemento[i].x
Elemento2[i].y = ( Distancia / Elemento[i].z ) * Elemento[i].y;
+
    Elemento2[i].y = (Distancia / Elemento[i].z) * Elemento[i].y
 
Fim para
 
Fim para
</pre>
+
</syntaxhighlight>Ou, em Python:<syntaxhighlight lang="python3">
 +
DISTANCIA = 300
 +
 
 +
def projetar_ponto(ponto):
 +
    x, y, z = ponto
 +
    fator = DISTANCIA / z
 +
    return (fator * x, fator * y)
 +
 
 +
elemento = [(x, y, z) for x, y, z in ...]  # seus pontos 3D
 +
elemento2 = [projetar_ponto(p) for p in elemento]
 +
</syntaxhighlight>Com isso, criamos um novo conjunto de pontos Elemento2, que representa a projeção do objeto original (Elemento) no plano de visualização.
  
Tendo sido criado um novo elemento (Elemento2), que é a representação do elemento original (Elemento) no plano de projeção, então não mais iremos apresentar o elemento original, mas apenas o elemento que é a sua representação no plano de projeção.
+
A partir deste ponto, não exibiremos mais o objeto original, mas sim sua representação projetada.
  
O resultado do algoritmo aplicado em um objeto tridimensional (cubo), originalmente no espaço euclidiano (Figura 2), é apresentado na Figura abaixo.
 
  
<center>[[Image:Perspectiva_cubo.png]]</center>
 
<center><small><b>Figura 2: Imagem do cubo isométrico (espaço euclidiano).</b></small></center>
 
  
<center>[[Image:Perspectiva_cubo_true.png]]</center>
+
O resultado dessa transformação, aplicada a um cubo originalmente situado no espaço euclidiano (Figura 2), é apresentado na Figura 3, agora sob uma perspectiva central.
<center><small><b>Figura 3: Imagem do cubo em perspectiva.</b></small></center>
+
[[Arquivo:Perspectiva cubo.png|centro|miniaturadaimagem|<small><b>Figura 2: Imagem do cubo isométrico (espaço euclidiano).</b></small>]]
 +
[[Arquivo:Perspectiva cubo true.png|centro|miniaturadaimagem|<small><b>Figura 3: Imagem do cubo em perspectiva.</b></small>]]
  
 
= Implementação =
 
= Implementação =
  
Arquivo space.py.
+
Script cubo3d.py.
  
 
<syntaxhighlight lang="python" n="">
 
<syntaxhighlight lang="python" n="">
 +
from __future__ import annotations
 
import sys
 
import sys
 
import math
 
import math
 
import pygame
 
import pygame
  
# Configuration
+
# Configuração
SPEED = 0.1
+
SPEED: float = 0.1
width = 800
+
WIDTH: int = 800
height = 600
+
HEIGHT:  int = 600
Color_screen = (0, 0, 0)
+
Color_screen: pygame.Color = (255, 255, 255)
Color_line = (255, 255, 255)
+
Color_line: pygame.Color = (0, 0, 0)
conical = False
+
CONICAL: bool = False
DISTANCE = 300
+
DISTANCE: float = 300
 +
 
  
 +
# -------------------------
 +
# Classe Point
 +
# -------------------------
 
class Point:
 
class Point:
    x = None
+
     def __init__(self, x: float, y: float, z: float):
    y = None
+
         self.x: float = x                              # vetor x
    z = None
+
         self.y: float = y                              # vetor y
 
+
         self.z: float = z                              # vetor z
     def __init__(self, vet):
 
        self.x = vet[0]
 
        self.y = vet[1]
 
        self.z = vet[2]
 
 
 
    def copy(self, p):
 
         self.x = p.x
 
         self.y = p.y
 
         self.z = p.z
 
  
     def get(self):
+
     def copy(self) -> Point:                           # Cria uma copia do ponto
         return [self.x, self.y, self.z]
+
         return Point(self.x, self.y, self.z)
  
  
 +
# -------------------------
 +
# Classe Cube
 +
# -------------------------
 
class Cube:
 
class Cube:
    screen = None
 
    point = []
 
 
 
     def __init__(self, screen):
 
     def __init__(self, screen):
         self.screen = screen
+
         self.screen: pygame.display = screen           # Referencia a screen do pygame
         self.point += [Point([0, 0, 0])]
+
         self.points: list[Point] = [                   # Criacao dos pontos do cubo
        self.point += [Point([-50, -50, -50])]
+
            Point(0, 0, 0),
        self.point += [Point([+50, -50, -50])]
+
            Point(-50, -50, -50),
        self.point += [Point([+50, +50, -50])]
+
            Point(+50, -50, -50),
        self.point += [Point([-50, +50, -50])]
+
            Point(+50, +50, -50),
        self.point += [Point([-50, -50, +50])]
+
            Point(-50, +50, -50),
        self.point += [Point([+50, -50, +50])]
+
            Point(-50, -50, +50),
        self.point += [Point([+50, +50, +50])]
+
            Point(+50, -50, +50),
        self.point += [Point([-50, +50, +50])]
+
            Point(+50, +50, +50),
         self.translate(0, 0, 300)
+
            Point(-50, +50, +50),
 +
        ]
 +
         self.translate(0, 0, 300)                       # Move para uma posicao inicial longe da tela
  
     def translate(self, tx, ty, tz) -> None:
+
     def translate(self, tx, ty, tz) -> None:             # Translada o cubo
         for p in self.point:
+
         for p in self.points:                           # Para cada ponto executa a matriz de translacao
             p.x += tx
+
             p.x += tx                                    # Soma x a tx
             p.y += ty
+
             p.y += ty                                    # Soma y a ty
             p.z += tz
+
             p.z += tz                                    # Soma z a tz
  
     def scale(self, sx, sy, sz) -> None:
+
     def scale(self, sx, sy, sz) -> None:                 # Executa a escala no cubo
         pivot = Point(self.point[0].get())
+
         pivot = self.points[0].copy()                   # Cria uma copia do ponto pivo
         self.translate(-pivot.x, -pivot.y, -pivot.z)
+
         self.translate(-pivot.x, -pivot.y, -pivot.z)     # Translada para a origem com referencia ao pivo
         for p in self.point:
+
         for p in self.point:                             # Executa a matriz de escala para cada ponto
 
             p.x *= sx
 
             p.x *= sx
 
             p.y *= sy
 
             p.y *= sy
 
             p.z *= sz
 
             p.z *= sz
         self.translate(pivot.x, pivot.y, pivot.z)
+
         self.translate(pivot.x, pivot.y, pivot.z)       # Translada da origem de volta ao pivo original
 
 
    def rotate_x(self, angle) -> None:
 
        angle /= 100
 
        pivot = Point(self.point[0].get())
 
        self.translate(-pivot.x, -pivot.y, -pivot.z)
 
        for p in self.point:
 
            y = p.y
 
            z = p.z
 
            p.y = (y * math.cos(angle)) - (z * math.sin(angle))
 
            p.z = (z * math.cos(angle)) + (y * math.sin(angle))
 
        self.translate(pivot.x, pivot.y, pivot.z)
 
  
     def rotate_y(self, angle) -> None:
+
     def rotate(self, angle, axis):                       # Executa a rotacao do cubo
         angle /= 100
+
         angle = math.radians(angle)                      # Converter para radianos
         pivot = Point(self.point[0].get())
+
         pivot = self.points[0].copy()                   # Faz uma copia do ponto pivo
         self.translate(-pivot.x, -pivot.y, -pivot.z)
+
         self.translate(-pivot.x, -pivot.y, -pivot.z)     # Translada para a origem com referência ao pivo
         for p in self.point:
+
         for p in self.points:                           # Executa a matriz de rotacao em cada ponto e cada vetor
             x = p.x
+
             x, y, z = p.x, p.y, p.z
             z = p.z
+
             if axis == "x":
             p.x = (x * math.cos(angle)) - (z * math.sin(angle))
+
                p.y = y * math.cos(angle) - z * math.sin(angle)
            p.z = (z * math.cos(angle)) + (x * math.sin(angle))
+
                p.z = z * math.cos(angle) + y * math.sin(angle)
 +
             elif axis == "y":
 +
                p.x = x * math.cos(angle) - z * math.sin(angle)
 +
                p.z = z * math.cos(angle) + x * math.sin(angle)
 +
            elif axis == "z":
 +
                p.x = x * math.cos(angle) - y * math.sin(angle)
 +
                p.y = y * math.cos(angle) + x * math.sin(angle)
 
         self.translate(pivot.x, pivot.y, pivot.z)
 
         self.translate(pivot.x, pivot.y, pivot.z)
  
     def rotate_z(self, angle) -> None:
+
    # Executa visualizacao da projecao conica / perspectiva se ativa
         angle /= 100
+
     def perspective(self, p) -> Point:
        pivot = Point(self.point[0].get())
+
         if CONICAL and p.z != 0:
        self.translate(-pivot.x, -pivot.y, -pivot.z)
+
             scale = DISTANCE / p.z
        for p in self.point:
+
             return Point(p.x * scale, p.y * scale, p.z)
             x = p.x
+
         return Point(p.x, p.y, p.z)
            y = p.y
 
             p.x = (x * math.cos(angle)) - (y * math.sin(angle))
 
            p.y = (y * math.cos(angle)) + (x * math.sin(angle))
 
         self.translate(pivot.x, pivot.y, pivot.z)
 
  
 
     def line(self, p1, p2) -> None:
 
     def line(self, p1, p2) -> None:
 
         pygame.draw.line(self.screen, Color_line, (p1.x, p1.y), (p2.x, p2.y))
 
         pygame.draw.line(self.screen, Color_line, (p1.x, p1.y), (p2.x, p2.y))
  
     def perpective(self, i, point) -> None:
+
     # Desenha o cubo
        if conical and point.z != 0:
 
            point.x = (DISTANCE / point.z) * point.x
 
            point.y = (DISTANCE / point.z) * point.y
 
 
 
 
     def draw(self) -> None:
 
     def draw(self) -> None:
 
         p = []
 
         p = []
 
         for i in range(9):
 
         for i in range(9):
             point = Point(self.point[i].get())
+
             point = self.perspective(self.points[i])
            self.perpective(i, point)
+
             point.x += WIDTH / 2
             point.x += 400
+
             point.y += HEIGHT / 2
             point.y += 300
+
             p.append(point)
             p += [point]
 
  
        self.screen.fill(Color_screen)
 
 
         self.line(p[0], p[0])  # draw point over pivot
 
         self.line(p[0], p[0])  # draw point over pivot
 
         for i in range(3):
 
         for i in range(3):
Linha 182: Linha 208:
 
         self.line(p[8], p[4])
 
         self.line(p[8], p[4])
 
         self.line(p[5], p[8])
 
         self.line(p[5], p[8])
        pygame.display.flip()
 
  
 
     def change(self, move):
 
     def change(self, move):
Linha 198: Linha 223:
 
             self.translate(0, 0, -SPEED)
 
             self.translate(0, 0, -SPEED)
 
         elif move == "rotate_y_left":
 
         elif move == "rotate_y_left":
             self.rotate_y(-SPEED)
+
             self.rotate(-SPEED, "y")
 
         elif move == "rotate_y_right":
 
         elif move == "rotate_y_right":
             self.rotate_y(SPEED)
+
             self.rotate(SPEED, "y")
 
         elif move == "rotate_x_up":
 
         elif move == "rotate_x_up":
             self.rotate_x(SPEED)
+
             self.rotate(SPEED, "x")
 
         elif move == "rotate_x_down":
 
         elif move == "rotate_x_down":
             self.rotate_x(-SPEED)
+
             self.rotate(-SPEED, "x")
 
         elif move == "rotate_z_right":
 
         elif move == "rotate_z_right":
             self.rotate_z(SPEED)
+
             self.rotate(SPEED, "z")
 
         elif move == "rotate_z_left":
 
         elif move == "rotate_z_left":
             self.rotate_z(-SPEED)
+
             self.rotate(-SPEED, "z")
 +
 
 +
 
 +
def show_help(screen, font):
 +
    help = [
 +
        font.render("ESC: Fecha o programa", True, (200, 200, 200)),
 +
        font.render("A, D: Rotaciona no eixo X", True, (200, 200, 200)),
 +
        font.render("S, W: Rotaciona mo eixo Y", True, (200, 200, 200)),
 +
        font.render("Z, X: Rotaciona mo eixo Z", True, (200, 200, 200)),
 +
        font.render("Setas ESQUERDA, DIREITA: Translada mo eixo X", True, (200, 200, 200)),
 +
        font.render("Setas CIMA, BAIXO: Translada mo eixo Y", True, (200, 200, 200)),
 +
        font.render("Q, E: Translada no eixo Z", True, (200, 200, 200)),
 +
        font.render("C: Liga/desliga a projeção cônica", True, (200, 200, 200)),
 +
    ]
 +
    help_pos_y = 10
 +
    for i in range(len(help)):
 +
        screen.blit(help[i], (10, help_pos_y))
 +
        help_pos_y += 25
 +
 
  
 
def main():
 
def main():
     global conical
+
     global CONICAL
     screen = pygame.display.set_mode((width, height))
+
    pygame.init()
 +
     screen = pygame.display.set_mode((WIDTH, HEIGHT))
 +
    font = pygame.font.SysFont(None, 22)
 
     cube = Cube(screen)
 
     cube = Cube(screen)
 
     cube.draw()
 
     cube.draw()
Linha 246: Linha 291:
 
                     move = "rotate_z_left"
 
                     move = "rotate_z_left"
 
                 elif event.key == pygame.K_c:
 
                 elif event.key == pygame.K_c:
                     conical = not conical
+
                     CONICAL = not CONICAL
 
                 elif event.key == pygame.K_ESCAPE:
 
                 elif event.key == pygame.K_ESCAPE:
 +
                    pygame.quit()
 
                     sys.exit(0)
 
                     sys.exit(0)
 
             elif event.type == pygame.KEYUP:
 
             elif event.type == pygame.KEYUP:
 
                 move = ""
 
                 move = ""
 
         cube.change(move)
 
         cube.change(move)
 +
 +
        screen.fill(Color_screen)
 
         cube.draw()
 
         cube.draw()
 +
        show_help(screen, font)
 +
 +
        pygame.display.flip()
 +
  
 
if __name__ == "__main__":
 
if __name__ == "__main__":
 
     main()
 
     main()
 +
 
</syntaxhighlight>
 
</syntaxhighlight>

Edição atual tal como às 09h18min de 16 de maio de 2025

Afluentes: Computação Gráfica

Introdução

Para representarmos um espaço tridimensional — tal como percebido pelos olhos humanos — em um plano bidimensional, como a tela do computador, recorremos a uma das mais importantes descobertas do mundo das artes: a perspectiva.

Desenvolvida a partir de análises visuais e necessidades construtivas, a projeção em perspectiva surge durante o Renascimento, na busca por soluções geométricas para a construção da cúpula da Catedral de Florença, pelo arquiteto italiano Filippo Brunelleschi (1377–1446).

Essa técnica introduziu o realismo na pintura e no desenho, oferecendo uma maneira sistemática de representar a profundidade — ou melhor, a ilusão dela — sobre superfícies planas.

A projeção perspectiva produz uma imagem visualmente realista, embora não preserve as medidas reais dos objetos, pois executa uma transformação dentro do espaço tridimensional para representar a cena a partir de um ponto de observação a uma distância finita.

Na projeção perspectiva, as coordenadas dos pontos projetados são obtidas pela intersecção dos raios projetores com o plano de projeção, conforme ilustrado na Figura 1.

Figura 1: Representação da Visualização em Perspectiva.

Termos

Sendo que:

  • (x, y, z): coordenadas de um ponto no espaço 3D (a cena que está sendo projetada).
  • f: a distância focal, que representa o quão longe está o plano da imagem do centro da projeção (o "olho" da câmera).
  • (x′,y′): coordenadas do ponto projetado no plano da imagem, ou seja, no plano 2D.
  • z é a distância do observador até o ponto original;

Ao aplicarmos o ponto do objeto no plano da imagem, iremos criar o ponto de projeção. Dessa forma, estamos aplicando uma transformação do espaço tridimensional para o espaço bidimensional.

Interpretação

Imagine que a câmera está olhando na direção do eixo Z, com o plano de projeção paralelo ao plano XY, colocado a uma distância f do centro (em z = f). O ponto 3D é então projetado em linha reta até esse plano.

Dessa forma, temos que x' dividido por x é igual a f dividido por x que implica em x' é igual a f dividido por z, multiplicado por x.

Logo, para encontrar o x' que é a representação no plano do x do espaço 3D, usamos a fórmula x' = f dividido por z, multiplicado por x:

E o mesmo se aplica a y ou seja, y' é igual a f dividido por z, multiplicado por y:

O z aqui é crucial — ele representa a profundidade. Quanto mais longe o objeto estiver (maior z), menor ele aparecerá na imagem — exatamente como nossos olhos percebem o mundo. Uma verdadeira dança de escala baseada na profundidade.

Formalizando mais...

Podemos ver essa projeção como uma transformação projetiva que leva um ponto 3D para 2D, ignorando a componente de profundidade na imagem. Em coordenadas homogêneas:

Para aplicarmos, fazemos uma operação de matrizes. Temos que a matriz vertical de três linhas e uma coluna, contendo x', y' e 1 é igual a matriz de três por quatro contendo na primeira linha: f, 0, 0 e 0, na segunda linha: 0, f, 0 e 0 e na terceira linha: 0, 0, 1 e 0, multiplicado pela matriz vertical de quatro por um contendo z, y, z e 1 da imagem 3D. Isso implica nas fórmulas para gerar x' e y' apresentadas anteriormente.

Essa abordagem é usada em gráficos computacionais, visão computacional, realidade aumentada, renderização 3D, etc.

E para implementar?

Tendo as equações já simplificadas, podemos agora aplicá-las na programação.

O exemplo a seguir apresenta, em forma de algoritmo, a função de transformação do espaço euclidiano tridimensional para sua projeção cônica na tela do nosso dispositivo computacional.

Distancia = 300
Elemento2 = Elemento

Para i = 0 até Quantidade_de_pontos fazer:
    Elemento2[i].x = (Distancia / Elemento[i].z) * Elemento[i].x
    Elemento2[i].y = (Distancia / Elemento[i].z) * Elemento[i].y
Fim para

Ou, em Python:

DISTANCIA = 300

def projetar_ponto(ponto):
    x, y, z = ponto
    fator = DISTANCIA / z
    return (fator * x, fator * y)

elemento = [(x, y, z) for x, y, z in ...]  # seus pontos 3D
elemento2 = [projetar_ponto(p) for p in elemento]

Com isso, criamos um novo conjunto de pontos Elemento2, que representa a projeção do objeto original (Elemento) no plano de visualização.

A partir deste ponto, não exibiremos mais o objeto original, mas sim sua representação projetada.


O resultado dessa transformação, aplicada a um cubo originalmente situado no espaço euclidiano (Figura 2), é apresentado na Figura 3, agora sob uma perspectiva central.

Figura 2: Imagem do cubo isométrico (espaço euclidiano).
Figura 3: Imagem do cubo em perspectiva.

Implementação

Script cubo3d.py.

from __future__ import annotations
import sys
import math
import pygame

# Configuração
SPEED: float = 0.1
WIDTH: int = 800
HEIGHT:  int = 600
Color_screen: pygame.Color = (255, 255, 255)
Color_line: pygame.Color = (0, 0, 0)
CONICAL: bool = False
DISTANCE: float = 300


# -------------------------
# Classe Point
# -------------------------
class Point:
    def __init__(self, x: float, y: float, z: float):
        self.x: float = x                               # vetor x
        self.y: float = y                               # vetor y
        self.z: float = z                               # vetor z

    def copy(self) -> Point:                            # Cria uma copia do ponto
        return Point(self.x, self.y, self.z)


# -------------------------
# Classe Cube
# -------------------------
class Cube:
    def __init__(self, screen):
        self.screen: pygame.display = screen            # Referencia a screen do pygame
        self.points: list[Point] = [                    # Criacao dos pontos do cubo
            Point(0, 0, 0),
            Point(-50, -50, -50),
            Point(+50, -50, -50),
            Point(+50, +50, -50),
            Point(-50, +50, -50),
            Point(-50, -50, +50),
            Point(+50, -50, +50),
            Point(+50, +50, +50),
            Point(-50, +50, +50),
        ]
        self.translate(0, 0, 300)                        # Move para uma posicao inicial longe da tela

    def translate(self, tx, ty, tz) -> None:             # Translada o cubo
        for p in self.points:                            # Para cada ponto executa a matriz de translacao
            p.x += tx                                    # Soma x a tx
            p.y += ty                                    # Soma y a ty
            p.z += tz                                    # Soma z a tz

    def scale(self, sx, sy, sz) -> None:                 # Executa a escala no cubo
        pivot = self.points[0].copy()                    # Cria uma copia do ponto pivo
        self.translate(-pivot.x, -pivot.y, -pivot.z)     # Translada para a origem com referencia ao pivo
        for p in self.point:                             # Executa a matriz de escala para cada ponto
            p.x *= sx
            p.y *= sy
            p.z *= sz
        self.translate(pivot.x, pivot.y, pivot.z)        # Translada da origem de volta ao pivo original

    def rotate(self, angle, axis):                       # Executa a rotacao do cubo
        angle = math.radians(angle)                      # Converter para radianos
        pivot = self.points[0].copy()                    # Faz uma copia do ponto pivo
        self.translate(-pivot.x, -pivot.y, -pivot.z)     # Translada para a origem com referência ao pivo
        for p in self.points:                            # Executa a matriz de rotacao em cada ponto e cada vetor
            x, y, z = p.x, p.y, p.z
            if axis == "x":
                p.y = y * math.cos(angle) - z * math.sin(angle)
                p.z = z * math.cos(angle) + y * math.sin(angle)
            elif axis == "y":
                p.x = x * math.cos(angle) - z * math.sin(angle)
                p.z = z * math.cos(angle) + x * math.sin(angle)
            elif axis == "z":
                p.x = x * math.cos(angle) - y * math.sin(angle)
                p.y = y * math.cos(angle) + x * math.sin(angle)
        self.translate(pivot.x, pivot.y, pivot.z)

    # Executa visualizacao da projecao conica / perspectiva se ativa
    def perspective(self, p) -> Point:
        if CONICAL and p.z != 0:
            scale = DISTANCE / p.z
            return Point(p.x * scale, p.y * scale, p.z)
        return Point(p.x, p.y, p.z)

    def line(self, p1, p2) -> None:
        pygame.draw.line(self.screen, Color_line, (p1.x, p1.y), (p2.x, p2.y))

    # Desenha o cubo
    def draw(self) -> None:
        p = []
        for i in range(9):
            point = self.perspective(self.points[i])
            point.x += WIDTH / 2
            point.y += HEIGHT / 2
            p.append(point)

        self.line(p[0], p[0])  # draw point over pivot
        for i in range(3):
            self.line(p[i + 1], p[i + 2])
            self.line(p[i + 5], p[i + 5 + 1])
            self.line(p[i + 1], p[i + 5])
        self.line(p[4], p[1])
        self.line(p[8], p[4])
        self.line(p[5], p[8])

    def change(self, move):
        if move == "translate_left":
            self.translate(-SPEED, 0, 0)
        elif move == "translate_right":
            self.translate(SPEED, 0, 0)
        elif move == "translate_up":
            self.translate(0, -SPEED, 0)
        elif move == "translate_down":
            self.translate(0, SPEED, 0)
        elif move == "translate_front":
            self.translate(0, 0, SPEED)
        elif move == "translate_back":
            self.translate(0, 0, -SPEED)
        elif move == "rotate_y_left":
            self.rotate(-SPEED, "y")
        elif move == "rotate_y_right":
            self.rotate(SPEED, "y")
        elif move == "rotate_x_up":
            self.rotate(SPEED, "x")
        elif move == "rotate_x_down":
            self.rotate(-SPEED, "x")
        elif move == "rotate_z_right":
            self.rotate(SPEED, "z")
        elif move == "rotate_z_left":
            self.rotate(-SPEED, "z")


def show_help(screen, font):
    help = [
        font.render("ESC: Fecha o programa", True, (200, 200, 200)),
        font.render("A, D: Rotaciona no eixo X", True, (200, 200, 200)),
        font.render("S, W: Rotaciona mo eixo Y", True, (200, 200, 200)),
        font.render("Z, X: Rotaciona mo eixo Z", True, (200, 200, 200)),
        font.render("Setas ESQUERDA, DIREITA: Translada mo eixo X", True, (200, 200, 200)),
        font.render("Setas CIMA, BAIXO: Translada mo eixo Y", True, (200, 200, 200)),
        font.render("Q, E: Translada no eixo Z", True, (200, 200, 200)),
        font.render("C: Liga/desliga a projeção cônica", True, (200, 200, 200)),
    ]
    help_pos_y = 10
    for i in range(len(help)):
        screen.blit(help[i], (10, help_pos_y))
        help_pos_y += 25


def main():
    global CONICAL
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    font = pygame.font.SysFont(None, 22)
    cube = Cube(screen)
    cube.draw()
    move = ""
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit(0)
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_LEFT:
                    move = "translate_left"
                elif event.key == pygame.K_RIGHT:
                    move = "translate_right"
                elif event.key == pygame.K_DOWN:
                    move = "translate_down"
                elif event.key == pygame.K_UP:
                    move = "translate_up"
                elif event.key == pygame.K_q:
                    move = "translate_back"
                elif event.key == pygame.K_e:
                    move = "translate_front"
                elif event.key == pygame.K_a:
                    move = "rotate_y_left"
                elif event.key == pygame.K_d:
                    move = "rotate_y_right"
                elif event.key == pygame.K_w:
                    move = "rotate_x_up"
                elif event.key == pygame.K_s:
                    move = "rotate_x_down"
                elif event.key == pygame.K_x:
                    move = "rotate_z_right"
                elif event.key == pygame.K_z:
                    move = "rotate_z_left"
                elif event.key == pygame.K_c:
                    CONICAL = not CONICAL
                elif event.key == pygame.K_ESCAPE:
                    pygame.quit()
                    sys.exit(0)
            elif event.type == pygame.KEYUP:
                move = ""
        cube.change(move)

        screen.fill(Color_screen)
        cube.draw()
        show_help(screen, font)

        pygame.display.flip()


if __name__ == "__main__":
    main()