Python para Desenvolvedores

2ª edição, revisada e ampliada

Capítulo 37: Imagens em três dimensões


Os formatos matricial e vetorial representam imagens bidimensionais no computador de forma adequada para a maior parte das aplicações. Porém, elas são limitadas em vários aspectos, principalmente para simulações, pois mundo que vivemos tem três dimensões (3D).

Uma cena 3D é composta por objetos, que representam sólidos, fontes de luz e câmeras. Os objetos sólidos geralmente são representados por malhas (meshes), que são conjunto de pontos (vértices). Estes possuem coordenadas x, y e z. Os pontos são interligados por linhas (arestas) que formam as superfícies (faces) dos objetos. Conjuntos de linhas que representam as malhas são chamados de estruturas de arame (wireframes).

Cena 3D

Objetos podem usar um ou mais materiais e estes podem ter várias características, tais como cor, transparência e sombreamento, que é a forma como o material responde a iluminação da cena. Além disso, o material pode ter uma ou mais texturas associadas.

Texturas são compostas por imagens de duas dimensões que podem ser usadas nos materiais aplicados as superfícies dos objetos, alterando várias propriedades, tais como reflexão, transparência e enrugamento (bump) da superfície.

Em uma cena 3D, os objetos podem modificados através de transformações, tais como translação (mover de uma posição para outra), rotação (girar em torno de um eixo) e redimensionamento (mudar de tamanho em uma ou mais dimensões).

Engrenagens 3D

Para renderizar, ou seja, gerar a imagem final, é necessário fazer uma série de cálculos complexos para aplicar iluminação e perspectiva aos objetos da cena. Entre os algoritmos usados para renderização, um dos mais conhecidos é o chamado raytrace, no qual os raios de luz são calculados da câmera até as fontes de luz. Com isso, são evitados cálculos desnecessários dos raios que não chegam até a câmera.

Um dos usos mais populares da tecnologia 3D é em animações. A técnica mais comum de animação em 3D é chamada de keyframe. Nela, o objeto a ser animado é posicionado em locais diferentes em momentos chave da animação, e o software se encarrega de calcular os quadros intermediários.

Muitos aplicativos 3D utilizam bibliotecas que implementam a especificação OpenGL (Open Graphics Library), que define uma API independente de plataforma e de linguagem, para a manipulação de gráficos 3D, permitindo a renderização em tempo real acelerada por hardware. Sua característica mais marcante é a performance. Mesa 3D é a implementação livre mais conhecida e está amplamente disponível em distribuições de Linux e BSD.

VPython

VPython é um pacote que permite criar e animar modelos simples em três dimensões. Seu objetivo é facilitar a criação rápida de simulações e protótipos que não requerem soluções complexas.

O VPython provê iluminação, controle de câmera e tratamento de eventos de mouse (rotação e zoom) automaticamente. Os objetos podem ser criados interativamente no interpretador, que a janela 3D do VPython é atualizada de acordo.

Exemplo:

In [ ]:
"""
Hexaedro
"""

# VPython
import visual

# Coordenadas para os vértices e arestas
coords = (-3, 3)

# Cor do vértice
cor1 = (0.9, 0.9, 1.0)

# Cor da aresta
cor2 = (0.5, 0.5, 0.6)

# Desenha esferas nos vértices
for x in coords:
    for y in coords:
        for z in coords:
            # pos é a posição do centro da esfera
            visual.sphere(pos=(x, y, z), color=cor1)

# Desenha os cilindros das arestas
for x in coords:
    for z in coords:
        # pos é a posição do centro da base do cilindro
        # radius é o raio da base do cilindro
        # axis é o eixo do cilindro
        visual.cylinder(pos=(x, 3, z), color=cor2,
            radius=0.25, axis=(0, -6, 0))

    for y in coords:
        visual.cylinder(pos=(x, y, 3), color=cor2,
            radius=0.25, axis=(0, 0, -6))

for y in coords:
    for z in coords:
        visual.cylinder(pos=(3, y, z), color=cor2,
            radius=0.25, axis=(-6, 0, 0))

Janela 3D:

Visual Python

Os objetos 3D do VPython podem ser agrupados em quadros (frames), que podem ser movidos e rotacionados.

É possível animar os objetos 3D usando laços. Para controlar a velocidade da animação, o VPython provê a função rate(), que pausa animação pelo inverso do argumento em segundos.

Exemplo de quadro e animação:

In [ ]:
"""
Octaedro animado
"""

from visual import *

# Cores
azul = (0.25, 0.25, 0.50)
verde = (0.25, 0.50, 0.25)

# Eixo de rotação
eixo = (0, 1, 0)

# Cria um frame alinhado com o eixo de rotação
fr = frame(axis=eixo)

# O fundo da caixa
box(pos=(0, -0.5, 0), color=azul,
    size=(10.0, 0.5, 8.0))

# O bordas da caixa
box(pos=(0, -0.5, 4.0), color=azul,
    size=(11.0, 1.0, 1.0))
box(pos=(0, -0.5, -4.0), color=azul,
    size=(11.0, 1.0, 1.0))
box(pos=(5.0, -0.5, 0), color=azul,
    size=(1.0, 1.0, 8.0))
box(pos=(-5.0, -0.5, 0), color=azul,
    size=(1.0, 1.0, 8.0))

# O pião
py1 = pyramid(frame=fr, pos=(1, 0, 0), color=verde,
    axis=(1, 0, 0))
py2 = pyramid(frame=fr, pos=(1, 0, 0), color=verde,
    axis=(-1, 0, 0))

# O pião anda no plano y = 0
delta_x = 0.01
delta_z = 0.01

print fr.axis

while True:

    # Inverte o sentido em x
    if abs(fr.x) > 4.2:
        delta_x = -delta_x

    # Inverte o sentido em z
    if abs(fr.z) > 3.1:
        delta_z = -delta_z

    fr.x += delta_x
    fr.z += delta_z

    # Rotaciona em Pi / 100 no eixo
    fr.rotate(angle=pi / 100, axis=eixo)

    # Espere 1 / 100 segundos
    rate(250)

Janela 3D:

Visual Python - Octaedro

O pacote inclui também um módulo de plotagem de gráficos, chamado graph.

Exemplo:

In [ ]:
# Módulo para plotagem de gráficos
from visual.graph import *

# Gráfico de linha simples
g1 = gcurve(color=(.8, .6, .3))

# Gráfico de barras
g2 = gvbars(delta=0.02, color=(.6, .4, .6))

# Limites do eixo X do gráfico
for x in arange(0., 10.1, .1):

    # plot() recebe X e Y
    # Plotando a curva
    g1.plot(pos=(x, 3 * sin(x) + cos(5 * x)))

    # Plotando as barras
    g2.plot(pos=(x, tan(x) * sin(4 * x)))

Janela de saída:

Visual Python - Grafico

Na versão 5, o VPython passou a incluir recursos como materiais prontos (como madeira, por exemplo) e controle de opacidade.

Exemplo:

In [ ]:
from visual import *

# Define posição inicial da câmera
scene.forward = (-0.1, -0.1, -0.1)

# Limpa a iluminação
scene.lights = []

# Define a iluminação ambiente
scene.ambient = (.1, .1, .2)

# Uma caixa de madeira
box(material=materials.wood)

# Uma esfera de material semi-transparente
sphere(radius=.2, pos=(-1, -0.3, 1), color=(.4, .5, .4),
    material=materials.rough, opacity=.5)

# Uma textura xadrez
x = 2 * (2 * (1, 0), 2 * (0, 1))
# Define a textura nova
mat = materials.texture(data=x, interpolate=False,
    mapping='rectangular')
# Caixa com a nova textura
box(axis=(0, 1, 0), size=(4, 4, 4), pos=(0, -3, 0), material=mat)

# A lâmpada é um frame composto por uma esfera e uma fonte de luz
c = (1., .9, .8)
lamp = frame(pos=(0, 1, 0))
# Define uma fonte de luz
local_light(frame=lamp, pos=(2, 1, 0), color=c)
# Define uma esfera com material emissor
sphere(frame=lamp, radius=0.1, pos=(2, 1, 0),
            color=c, material=materials.emissive)

while True:
    # Anima a lâmpada, rotacionando em torno do eixo Y
    lamp.rotate(axis=(0, 1, 0), angle=.1)
    rate(10)

Janela de saída:

Visual Python - Madeira

O VPython tem várias limitações. Ele não provê formas de criar e/ou manipular materiais ou texturas mais complexas, nem formas avançadas de iluminação ou detecção de colisões. Para cenas mais sofisticadas, existem outras soluções, como o Python Ogre e o Blender, que é um aplicativo de modelagem 3D que usa Python como linguagem script.

PyOpenGL

As bibliotecas OpenGL implementam uma API de baixo nível para manipulação de imagens 3D, permitindo o acesso aos recursos disponíveis no hardware de vídeo, e também torna o código independente da plataforma, pois emula por software as funcionalidades que não estiverem disponíveis no equipamento. Entre esses recursos temos: primitivas (linhas e polígonos), mapeamento de texturas, operações de transformação e iluminação.

A OpenGL funciona em um contexto, que tem seu estado alterado através das funções definidas na especificação. Este estado é mantido até que sofra uma nova alteração.

Complementando a biblioteca principal, a OpenGL Utility Library (GLU) é uma biblioteca com funções de alto nível, enquanto a OpenGL Utility Toolkit (GLUT) define rotinas independentes de plataforma para gerenciamento de janelas, entrada e contexto.

A GLUT é orientada a eventos, aos quais é possível se associar funções callback, que executam as chamadas OpenGL. A biblioteca tem uma rotina que monitora os eventos e evoca as funções quando necessário.

PyOpenGL é um pacote que permite que programas em Python utilizem as bibliotecas OpenGL, GLU e GLUT.

Exemplo:

In [ ]:
from sys import argv
from OpenGL.GL import *
from OpenGL.GLUT import *

def display():
    """
    Função callback que desenha na janela
    """
    
    # glClear limpa a janela com valores pré-determinados
    # GL_COLOR_BUFFER_BIT define que o buffer aceita escrita de cores
    # GL_DEPTH_BUFFER_BIT define que o buffer de profundidade será usado
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    rgba = [.8, .6, .4, .9]
    # glMaterial especifica os parâmetros do material que serão
    # usados no modelo de iluminação da cena (no formato RGBA)
    # GL_FRONT define que a face afetada pela função é a frontal
    # GL_AMBIENT especifica que o parâmetro é a reflexão de ambiente
    glMaterialfv(GL_FRONT, GL_AMBIENT, rgba)

    # GL_DIFFUSE especifica que o parâmetro é a reflexão difusa do material
    glMaterialfv(GL_FRONT, GL_DIFFUSE, rgba)

    # GL_SPECULAR especifica que o parâmetro é a reflexão especular
    glMaterialfv(GL_FRONT, GL_SPECULAR, rgba)

    # GL_EMISSION especifica que o parâmetro é a emissão do material
    # glMaterialfv(GL_FRONT, GL_EMISSION, rgba)

    # GL_SHININESS especifica o expoente usado pela reflexão especular
    glMaterialfv(GL_FRONT, GL_SHININESS, 120)

    # Desenha uma esfera sólida, com raio 0.5 e 128 divisões
    # na horizontal e na vertical
    glutSolidSphere(0.5, 128, 128)

    # Força a execução dos comandos da OpenGL
    glFlush()

#  Inicializa a biblioteca GLUT
glutInit(argv)

# glutInitDisplayMode configura o modo de exibição
# GLUT_SINGLE define o buffer como simples
# (também pode ser duplo, com GLUT_DOUBLE)
# GLUT_RGB seleciona o modo RGB
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB)

# Cria a janela principal
glutCreateWindow('Esfera')

# Configura a função callback que desenha na janela atual
glutDisplayFunc(display)

# Limpa a janela com a cor especificada
glClearColor(.25, .15, .1, 1.)

# Muda a matriz corrente para GL_PROJECTION
glMatrixMode(GL_PROJECTION)

# Carrega uma matriz identidade na matriz corrente
glLoadIdentity()

# Configurando os parâmetros para as fontes de luz
# GL_DIFFUSE define o parâmetro usado a luz difusa (no formato RGBA)
glLightfv(GL_LIGHT0, GL_DIFFUSE, [1., 1., 1., 1.])

# Os três parâmetros definem a posição da fonte luminosa
# O quarto define se a fonte é direcional (0) ou posicional (1)
glLightfv(GL_LIGHT0, GL_POSITION, [-5., 5., -5., 1.])

# Aplica os parâmetros de iluminação
glEnable(GL_LIGHTING)

# Inclui a fonte de luz 0 no calculo da iluminação
glEnable(GL_LIGHT0)

# Inicia o laço de eventos da GLUT
glutMainLoop()

Janela de saída:

PyOpenGL - Esfera

A biblioteca também oferece rotinas para fazer transformações, o que permite animar os objetos da cena.

Exemplo:

In [ ]:
from sys import argv
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

# Ângulo de rotação do objeto
ar = 0.

# Variação da rotação
dr = 1.

def resize(x, y):
    """
    Função callback que é evocada quando
    a janela muda de tamanho
    """

    # Limpa a vista
    glViewport(0, 0, x, y)

    # Seleciona a matriz de projeção
    glMatrixMode(GL_PROJECTION)

    # Limpa a matriz de projeção
    glLoadIdentity()
    
    # Calcula o aspecto da perspectiva
    gluPerspective(45., float(x)/float(y), 0.1, 100.0)
    
    # Seleciona a matriz de visualização
    glMatrixMode(GL_MODELVIEW)

def draw():
    """
    Função que desenha os objetos
    """
    global ar, dr

    # Limpa a janela e o buffer de profundidade
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    # Limpa a matriz de visualização
    glLoadIdentity()

    # Move o objeto (translação)
    # Parâmetros: x, y e z (deslocamento)
    glTranslatef(-0.5, -0.5, -4.)

    # Rotação (em graus)
    # Parâmetros: graus, x, y e z (eixo)
    glRotatef(ar, 1.0 , 1.0, 1.0)

    # Mudança de escala
    # Parâmetros: x, y e z (tamanho)
    glScalef(ar  / 1000, ar / 1000, ar / 1000)

    for i in xrange(0, 360, 10):

        # Rotação das faces do objeto
        glRotatef(10, 1.0 , 1.0, 1.0)

        # Inicia a criação de uma face retangular
        glBegin(GL_QUADS)

        # Define a cor que será usada para desenhar (R, G, B)
        glColor3f(.7, .5, .1)

        # Cria um vértice da face
        glVertex3f(0., 0., 0.)
        glColor3f(.7, .3, .1)
        glVertex3f(1., 0., 0.)
        glColor3f(.5, .1, .1)
        glVertex3f(1., 1., 0.)
        glColor3f(.7, .3, .1)
        glVertex3f(0., 1., 0.)

        # Termina a face
        glEnd()

    # Inverte a variação
    if ar > 1000: dr = -1
    if ar < 1: dr = 1
    ar  = ar + dr

    #  Troca o buffer, exibindo o que acabou de ser usado
    glutSwapBuffers()

def keyboard(*args):
    """
    Função callback para tratar eventos de teclado
    """
    # Testa se a tecla ESC foi apertada
    if args[0] == '\33': 
        raise SystemExit

if __name__ == '__main__':

    # Inicializa a GLUT
    glutInit(argv)

    # Seleciona o modo de exibição
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH)

    # Configura a resolução da janela do GLUT de 640 x 480
    glutInitWindowSize(640, 480)

    # Cria a janela do GLUT
    window = glutCreateWindow('Transformações')

    # Configura a função callback que desenha na janela atual
    glutDisplayFunc(draw)

    # Para exibir em tela cheia
    #glutFullScreen()

    # Registra a função para tratar redesenhar a janela quando
    # não há nada a fazer
    glutIdleFunc(draw)

    # Registra a função para redesenhar a janela quando
    # ela for redimensionada
    glutReshapeFunc(resize)

    # Registra a função para tratar eventos de teclado
    glutKeyboardFunc(keyboard)

    # Inicialização da janela
    # Limpa a imagem (fundo preto)
    glClearColor(0., 0., 0., 0.)

    # Limpa o buffer de profundidade
    glClearDepth(1.)
    
    # Configura o tipo do teste de profundidade
    glDepthFunc(GL_LESS)

    # Ativa o teste de profundidade
    glEnable(GL_DEPTH_TEST)

    # Configura o sombreamento suave
    glShadeModel(GL_SMOOTH)

    # Seleciona a matriz de projeção
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45., 640. / 480., .1, 100.)

    # Seleciona a matriz de visualização
    glMatrixMode(GL_MODELVIEW)

    # Inicia o laço de eventos da GLUT
    glutMainLoop()

Janela de saída:

PyOpenGL - Animando

A OpenGL pode ser integrada com toolkits gráficos, como wxWidgets e Qt, ao invés de usar a GLUT, e com isso, ser incorporada em aplicações GUI convencionais.

Uma das principais referências sobre OpenGL é o livro “OpenGL Programming Guide”, também conhecido como Red Book.

In [1]:
 
Out[1]: