Eu traduzi o código do livro Pro Java 6 3D Game Development no capítulo onde o autor introduz JOGL usando renderização ativa, que é a opção mais avançada e flexível para criação de jogos 3D usando o JOGL com a especificação JSR-231.
Com isso em mãos começa a ficar bem mais fácil entender e dominar o JOGL.
Só alterei um detalhe colocando as constantes em uma interface, e mantive um ou outro termo em inglês para manter os termos usados no JOGL.
Segue também em anexo!
[code]package cubo;
public interface Constantes {
int fpsPadrao = 60;
int larguraPainel = 512; // tamanho do painel
int alturaPainel = 512;
float incrementoMaximo = 10.0f; // para os incrementos de rotação
double distanciaZ = 7.0; //para a posição da câmera
/* Constantes para estatística
* grava estatísticas a cada 1 segundo (aproximadamente) */
long intervaloMaxStats = 1000000000L;
/* Número de interações com um atraso de adormecimento de 0 ms antes
* da thread de animação esperar por (yield) outras treads */
final int semAtrasosPorEspera = 16;
/* Número de renderizações que podem ser "puladas" nos loops de animação
* ou seja: o estado do jogo é atualizado, mas não renderizado */
int maxPulosRenderizacao = 5; // was 2;
/* Quantidade de valores de FPS (frames por
* segundo) guardados para obter uma média */
int numFps = 10;
}[/code]
[code]package cubo;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.text.DecimalFormat;
import javax.media.opengl.AWTGraphicsConfiguration;
import javax.media.opengl.AWTGraphicsDevice;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLDrawableFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
public class Janela extends JFrame implements WindowListener, Constantes {
private static final long serialVersionUID = 1L;
private TelaCuboGL telaCuboGL;
private JTextField rotacoesTF; // exibe as rotações do cubo
private DecimalFormat df = new DecimalFormat("0.#"); // 1 dp
public Janela(long periodo) {
super("CubeGL (Renderizacao Ativa)");
Container c = getContentPane();
c.setLayout( new BorderLayout() );
c.add(criaPainelRenderizacao(periodo), BorderLayout.CENTER);
rotacoesTF = new JTextField("Rotacoes: ");
rotacoesTF.setEditable(false);
c.add(rotacoesTF, BorderLayout.SOUTH);
addWindowListener(this);
pack();
setVisible(true);
}
/** cria a tela, dentro de um JPanel */
private JPanel criaPainelRenderizacao(long periodo) {
JPanel painelRenderizacao = new JPanel();
painelRenderizacao.setLayout( new BorderLayout() );
painelRenderizacao.setOpaque(false);
painelRenderizacao.setPreferredSize(new Dimension(larguraPainel, alturaPainel));
telaCuboGL = criaTela(periodo);
painelRenderizacao.add(telaCuboGL, BorderLayout.CENTER);
telaCuboGL.setFocusable(true);
telaCuboGL.requestFocus(); // agora a tela tem o foco, então recebe os eventos de teclado
// detecta o redimensionamento da janela, e reformata a tela de acordo
painelRenderizacao.addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent evt) {
Dimension d = evt.getComponent().getSize();
telaCuboGL.reshape(d.width, d.height);
} // end of componentResized()
});
return painelRenderizacao;
}
private TelaCuboGL criaTela(long periodo) {
// obtém uma configuração adequada para a tela AWT (para TelaCuboGL)
GLCapabilities caps = new GLCapabilities();
AWTGraphicsDevice dev = new AWTGraphicsDevice(null);
AWTGraphicsConfiguration awtConfig =
(AWTGraphicsConfiguration) GLDrawableFactory.getFactory().chooseGraphicsConfiguration(caps, null, dev);
GraphicsConfiguration config = null;
if (awtConfig != null) {
config = awtConfig.getGraphicsConfiguration();
}
return new TelaCuboGL(this, periodo, larguraPainel, alturaPainel, config, caps);
}
/** chamado pela TelaCuboGL para mostrar as rotações do cubo */
public void setRots(float rotX, float rotY, float rotZ) {
rotacoesTF.setText("Rotações: (" +
df.format(rotX) + ", " +
df.format(rotY) + ", " +
df.format(rotZ) + ")");
}
// ----------------- window listener methods -------------
public void windowActivated(WindowEvent e) {
telaCuboGL.continuaJogo();
}
public void windowDeactivated(WindowEvent e) {
telaCuboGL.pausaJogo();
}
public void windowDeiconified(WindowEvent e) {
telaCuboGL.continuaJogo();
}
public void windowIconified(WindowEvent e) {
telaCuboGL.pausaJogo();
}
public void windowClosing(WindowEvent e) {
telaCuboGL.finalizaJogo();
}
public void windowClosed(WindowEvent e) {}
public void windowOpened(WindowEvent e) {}
// -----------------------------------------
public static void main(String[] args) {
int fps = fpsPadrao;
if (args.length != 0)
fps = Integer.parseInt(args[0]);
long periodo = (long) 1000.0/fps;
System.out.println("fps: " + fps + "; período: " + periodo + " ms");
new Janela(periodo*1000000L); // ms --> nanosecs
}
}[/code]
[code]package cubo;
import java.awt.Canvas;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.text.DecimalFormat;
import java.util.Random;
import javax.media.opengl.GL;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLContext;
import javax.media.opengl.GLDrawable;
import javax.media.opengl.GLDrawableFactory;
import javax.media.opengl.glu.GLU;
public class TelaCuboGL extends Canvas implements Runnable, Constantes {
private static final long serialVersionUID = 1L;
// usado para coletar estatísticas
private long intervaloStats = 0L; // in ns
private long tempoAnteriorStats;
private long tempoTotalUsado = 0L;
private long tempoInicioJogo;
private int tempoGastoEmJogo = 0; // in seconds
private long contagemFrames = 0;
private double historicoFps[];
private long contagemStats = 0;
private double fpsMedio = 0.0;
private long renderizacoesPuladas = 0L;
private long totalRenderizacoesPuladas = 0L;
private double historicoUps[]; // Updates (atualizações) por segundo
private double upsMedio = 0.0;
private DecimalFormat df = new DecimalFormat("0.##"); // 2 dp
private DecimalFormat timedf = new DecimalFormat("0.####"); // 4 dp
// utilizado no fim do jogo
private volatile boolean gameOver = false;
// Vértices para um cubo com lados de 2 unidades centralizado em (0,0,0)
private float[][] verts = {
{-1.0f,-1.0f, 1.0f}, // vértice 0
{-1.0f, 1.0f, 1.0f}, // 1
{ 1.0f, 1.0f, 1.0f}, // 2
{ 1.0f,-1.0f, 1.0f}, // 3
{-1.0f,-1.0f,-1.0f}, // 4
{-1.0f, 1.0f,-1.0f}, // 5
{ 1.0f, 1.0f,-1.0f}, // 6
{ 1.0f,-1.0f,-1.0f}, // 7
};
int listaExibicaoCubo; // display list para exibir o cubo
private Janela janela; // referência à janela principal
private long periodo; // período entre renderizações em _nanosecs_
private Thread animador; // a thread responsável pelas animações
private volatile boolean isRodando = false; // Utilizado para finalizar a thread de animação
private volatile boolean isPausado = false;
// OpenGL
private GLDrawable drawable; // a superfície de renderização
private GLContext context; // o contexto de renderização (mantém informações de estado de renderização)
private GL gl;
private GLU glu;
// variáveis de rotação
private float rotX, rotY, rotZ; // rotações totais nos eixos x, y, z
private float incrX, incrY, incrZ; // incrementos para as rotações x, y, z
// dimensionamento da janela
private boolean isRedimensionado = false;
private int larguraAtualPainel, alturaAtualPainel;
public TelaCuboGL(Janela janela, long periodo, int larguraPainel,
int alturaPainel, GraphicsConfiguration config, GLCapabilities caps) {
super(config);
this.janela = janela;
this.periodo = periodo;
larguraAtualPainel = larguraPainel;
alturaAtualPainel = alturaPainel;
// Obtém uma superfície de renderização e um contexto para essa tela
drawable = GLDrawableFactory.getFactory().getGLDrawable(this, caps, null);
context = drawable.createContext(null);
// inicializa as variáveis de rotação
rotX = 0; rotY = 0; rotZ = 0;
Random random = new Random();
incrX = random.nextFloat()*incrementoMaximo; // 0 - incrementoMaximo graus
incrY = random.nextFloat()*incrementoMaximo;
incrZ = random.nextFloat()*incrementoMaximo;
// inicialização das estatísticas
historicoFps = new double[numFps];
historicoUps = new double[numFps];
for (int i=0; i < numFps; i++) {
historicoFps[i] = 0.0;
historicoUps[i] = 0.0;
}
}
/** Espera a tela ser adicionada ao JPanel antes de iniciar */
public void addNotify() {
super.addNotify(); // torna o componente visualizável
drawable.setRealized(true); // agora é possível renderizar na tela
// inicializa e inicia a thread de animação
if (animador == null || !isRodando) {
animador = new Thread(this);
animador.start();
}
}
// ------------- métodos do ciclo de vida do jogo ------------
// Chamados pelos listeners de janela do JFrame
/** chamado quando o JFrame é ativado / desiconificado */
public void continuaJogo() {
isPausado = false;
}
/** Chamado quando o JFrame é desativado / iconificado */
public void pausaJogo() {
isPausado = true;
}
/** Chamado quando o JFrame está sendo fechado */
public void finalizaJogo() {
isRodando = false;
}
// ----------------------------------------------
/** Chamado pelo ComponentListener do JFrame quando a janela é redimensionada
* Similar ao callback do reshape() no GLEventListener (capítulo anterior) */
public void reshape(int largura, int altura) {
isRedimensionado = true;
if (altura == 0)
altura = 1; // Para evitar divisão por zero na proporção de tamanho em resizeView()
larguraAtualPainel = largura;
alturaAtualPainel = altura;
}
/** Sobrescrevendo sem ação, já que a animação
* ocorrerá na tela OpenGL, não no Canvas */
public void update(Graphics g) { }
public void paint(Graphics g) { }
/** Inicializando a renderização e iniciando a geração de frames */
public void run() {
iniciaRenderizacao();
loopRenderizacao();
// descarta o contexto de renderização e sai
context.destroy();
System.exit(0);
} // end of run()
/** Traz o contexto de renderização para essa thread */
private void trazContexto() {
try {
while (context.makeCurrent() == GLContext.CONTEXT_NOT_CURRENT) {
// Apenas uma segurança extra que nem deve chegar a executar
System.out.println("Aguardando contexto...");
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/** Inicialização da renderização (similar ao callback
* init() do GLEventListener no capítulo anterior */
private void iniciaRenderizacao() {
trazContexto();
gl = context.getGL();
glu = new GLU();
redimensionaVisualizacao();
gl.glClearColor(0.17f, 0.65f, 0.92f, 0.0f); // cor de céu no fundo
// z- (profundidade) inicialização do buffer para remoção de superfícies ocultas
gl.glEnable(GL.GL_DEPTH_TEST);
// gl.glClearDepth(1.0f);
// gl.glDepthFunc(GL.GL_LEQUAL); // tipo de teste de profundidade
// gl.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);
/* Cria uma lista de exibição para desenhar o cubo
* É uma pré-compilação dos comandos, muito mais rápido
* do que executá-los a cada loop */
listaExibicaoCubo = gl.glGenLists(1);
gl.glNewList(listaExibicaoCubo, GL.GL_COMPILE);
desenhaCuboColorido(gl);
gl.glEndList();
/* Libera o contexto para que a trava AWT
* do X11 (linux) possa ser liberada */
context.release();
}
private void redimensionaVisualizacao() {
gl.glViewport(0, 0, larguraAtualPainel, alturaAtualPainel); // tamanho da área de desenho
gl.glMatrixMode(GL.GL_PROJECTION);
gl.glLoadIdentity();
// fov (abertura), proporção de tamanho, planos de corte próximo e distante
glu.gluPerspective(45.0, (float)larguraAtualPainel/(float)alturaAtualPainel, 1, 100); // 5, 100);
}
// ---------------- renderização baseada em frames -----------------------
/** Repetidamente atualiza, renderiza, põe na tela e adormece,
* mantendo um período fixo tão preciso quanto possível.
* Obtém e exibe estatísticas */
private void loopRenderizacao() {
// Variáveis de temporização
long tempoAntes, tempoDepois, diferencaTempo, tempoAdormecimento;
long tempoAdormecimentoExcedido = 0L;
int semAtrasos = 0;
long sobra = 0L;
tempoInicioJogo = System.nanoTime(); // J3DTimer.getValue();
tempoAnteriorStats = tempoInicioJogo;
tempoAntes = tempoInicioJogo;
isRodando = true;
while(isRodando) {
trazContexto();
atualizaJogo();
renderizaCena(); // renderizando
// Troca os buffers de fundo e de frante, tornando a nova renderização visível */
drawable.swapBuffers(); // Coloca a cena na tela
tempoDepois = System.nanoTime();
diferencaTempo = tempoDepois - tempoAntes;
tempoAdormecimento = (periodo - diferencaTempo) - tempoAdormecimentoExcedido;
if (tempoAdormecimento > 0) { // sobrou algum tempo nesse ciclo
try {
Thread.sleep(tempoAdormecimento/1000000L); // nano -> ms
} catch(InterruptedException ex){}
tempoAdormecimentoExcedido = (System.nanoTime() - tempoDepois) - tempoAdormecimento;
} else {
// sleepTime <= 0; esse ciclo levou mais tempo do que o período
sobra -= tempoAdormecimento; // guarda o valor da sobra de tempo
tempoAdormecimentoExcedido = 0L;
if (++semAtrasos >= semAtrasosPorEspera) {
Thread.yield(); // Dá a outra thread a oportunidade de executar
semAtrasos = 0;
}
}
tempoAntes = System.nanoTime(); // J3DTimer.getValue();
/* Se a renderização está tomando muito tempo, atualiza o estado do jogo
* sem renderizar a atualização, para que os updates por segundo estejam
* próximos da taxa de frames por segundo requerida */
int pulos = 0;
while((sobra > periodo) && (pulos < maxPulosRenderizacao)) {
sobra -= periodo;
atualizaJogo(); // atualiza, mas não renderiza
pulos++;
}
renderizacoesPuladas += pulos;
// Libera o contexto para que a trava AWT do X11 (linux) seja liberada
context.release();
armazenaStats();
}
exibeStats();
}
private void atualizaJogo() {
if (!isPausado && !gameOver) {
// atualiza as rotações
rotX = (rotX + incrX) % 360.0f; // % 360 faz retornar a zero no 360
rotY = (rotY + incrY) % 360.0f;
rotZ = (rotZ + incrZ) % 360.0f;
janela.setRots(rotX, rotY, rotZ);
}
}
// ------------------ métodos de renderização -----------------------------
private void renderizaCena() {
if (GLContext.getCurrent() == null) {
System.out.println("O contexto atual é nulo");
System.exit(0);
}
if (isRedimensionado) { // Redimensiona o drawable (desenhável) se necessário
redimensionaVisualizacao();
isRedimensionado = false;
}
// limpa a cor e os buffers de profundidade
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
gl.glMatrixMode(GL.GL_MODELVIEW);
gl.glLoadIdentity();
glu.gluLookAt(0,0,distanciaZ, 0,0,0, 0,1,0); // posiciona a câmera
// aplica rotações aos eixos x, y e z
gl.glRotatef(rotX, 1.0f, 0.0f, 0.0f);
gl.glRotatef(rotY, 0.0f, 1.0f, 0.0f);
gl.glRotatef(rotZ, 0.0f, 0.0f, 1.0f);
gl.glCallList(listaExibicaoCubo); // executa a lista de exibição pré-compilada para o cubo
// desenhaCuboColorido(gl); // OU pode-se executar os comandos diretamente (muito mais lento)
if (gameOver)
System.out.println("Game Over");
}
// Cubo de seis lados, com uma cor diferente em cada face
private void desenhaCuboColorido(GL gl) {
gl.glColor3f(1.0f, 0.0f, 0.0f); // vermelho
desenhaPoligono(gl, 0, 3, 2, 1); // face frontal
gl.glColor3f(0.0f, 1.0f, 0.0f); // verde
desenhaPoligono(gl, 2, 3, 7, 6); // direita
gl.glColor3f(0.0f, 0.0f, 1.0f); // azul
desenhaPoligono(gl, 3, 0, 4, 7); // fundo
gl.glColor3f(1.0f, 1.0f, 0.0f); // amarelo
desenhaPoligono(gl, 1, 2, 6, 5); // topo
gl.glColor3f(0.0f, 1.0f, 1.0f); // azul claro
desenhaPoligono(gl, 4, 5, 6, 7); // atrás
gl.glColor3f(1.0f, 0.0f, 1.0f); // roxo
desenhaPoligono(gl, 5, 4, 0, 1); // esquerda
}
// Os vértices do polígino vêm do vetor verts[]
private void desenhaPoligono(GL gl, int vIdx0, int vIdx1, int vIdx2, int vIdx3) {
gl.glBegin(GL.GL_POLYGON);
gl.glVertex3f( verts[vIdx0][0], verts[vIdx0][1], verts[vIdx0][2] );
gl.glVertex3f( verts[vIdx1][0], verts[vIdx1][1], verts[vIdx1][2] );
gl.glVertex3f( verts[vIdx2][0], verts[vIdx2][1], verts[vIdx2][2] );
gl.glVertex3f( verts[vIdx3][0], verts[vIdx3][1], verts[vIdx3][2] );
gl.glEnd();
}
// ----------------- métodos de estatísticas ------------------------
/** As estatísticas:
* - os períodos somados de todas as interações nesse intervalo
* (período é a quantidade de tempo que uma simples enteração deve levar),
* o tempo atual gasto nesse intervalo,
* o erro entre esses dois números;
*
* - A contagem total de frames, que é o número total de chamadas a run();
*
* - Os frames pulados nesse intervalo, o número total de frames
* pulados. Um pulo de frame é uma atualização do jogo sem a renderização correspondente;
*
* - O FPS (frames por segundo) e UPS (atualizações por segundo) nesse intervalo,
* o FPS e UPS médio sobre os últimos intervalos numFps
*
* Os dados são coletados a cada intervaloMaxStats (1 segundo).
*
* ----- MUDANÇAS -----
* Não exibe a saída, deixa isso para exibeStatus(). */
private void armazenaStats() {
contagemFrames++;
intervaloStats += periodo;
if (intervaloStats >= intervaloMaxStats) { // grava as estatísticas a cada intervaloMaxStats
long tempoAtual = System.nanoTime(); // J3DTimer.getValue();
tempoGastoEmJogo = (int) ((tempoAtual - tempoInicioJogo)/1000000000L); // ns --> secs
long tempoUsadoReal = tempoAtual - tempoAnteriorStats; // tempo desde a última coletagem de estatísticas
tempoTotalUsado += tempoUsadoReal;
double erroTemporizacao = ((double)(tempoUsadoReal - intervaloStats) / intervaloStats) * 100.0;
totalRenderizacoesPuladas += renderizacoesPuladas;
double fpsAtual = 0; // calculate the latest FPS and UPS
double upsAtual = 0;
if (tempoTotalUsado > 0) {
fpsAtual = (((double)contagemFrames / tempoTotalUsado) * 1000000000L);
upsAtual = (((double)(contagemFrames + totalRenderizacoesPuladas) / tempoTotalUsado) * 1000000000L);
}
// sarmazena o FPS e UPS mais recentes
historicoFps[ (int)contagemStats % numFps ] = fpsAtual;
historicoUps[ (int)contagemStats % numFps ] = upsAtual;
contagemStats ++;
double fpsTotal = 0.0; // totaliza os FPS e UPS armazenados
double upsTotal = 0.0;
for (int i=0; i < numFps; i++) {
fpsTotal += historicoFps[i];
upsTotal += historicoUps[i];
}
if (contagemStats < numFps) { // obtém a média de FPS e UPS
fpsMedio = fpsTotal/contagemStats;
upsMedio = upsTotal/contagemStats;
} else {
fpsMedio = fpsTotal/numFps;
upsMedio = upsTotal/numFps;
}
/*
System.out.println(timedf.format( (double) intervaloStats/1000000000L) + " " +
timedf.format((double) tempoUsadoReal/1000000000L) + "s " +
df.format(erroTemporizacao) + "% " +
contagemFrames + "c " +
renderizacoesPuladas + "/" + totalRenderizacoesPuladas + " skip; " +
df.format(fpsAtual) + " " + df.format(fpsMedio) + " afps; " +
df.format(upsAtual) + " " + df.format(upsMedio) + " aups" );
*/
renderizacoesPuladas = 0;
tempoAnteriorStats = tempoAtual;
intervaloStats = 0L; // reset
}
}
private void exibeStats() {
// System.out.println("Contagem/Perda de frames: " + contagemFrames + " / " + totalRenderizacoesPuladas);
System.out.println("FPS Médio: " + df.format(fpsMedio));
System.out.println("UPS Médio: " + df.format(upsMedio));
System.out.println("Tempo Gasto: " + tempoGastoEmJogo + " secs");
}
}[/code]