Campo de estrellas 3D

Click here to see in English

En los anteriores tutoriales Campo de estrellas 2D y Midiendo tiempos y optimizando Campo de estrellas 2D vimos como programar y optimizar un efecto de campo de estrellas en dos dimensiones, vamos ahora a hacer un sencillo efecto de campo de estrellas 3D.

En este efecto vamos a simular que viajamos en la cabina de una nave espacial y avanzamos entre las estrellas. Cada una de las estrellas va a tener una posición en el eje x, en el eje y en el eje z (profundidad), lo que haremos es mover la posición de cada estrella en el eje z hasta llegar a 0 y volveremos a empezar. Para pintar en la pantalla, debemos calcular la posición en 2D de una estrella en 3D, que es realmente sencillo: x2D = x3D / z3D e y2D = y3D / z3D. Nuestra pantalla en modo 0 es de 160x200, pero en 3D vamos a ampliar este rango en la x e y de cada estrella, ya que en caso contrario todas estarían en el centro de la pantalla hasta que la z fuera muy cercana a 0.

Este sería el código completo 3Dstars01.c (para sdcc):

////////////////////////////////////////////////////////////////////////
// 3Dstars01.c
// 3D Star Field
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned char GetMode0PixelColorByte(unsigned char nColor, unsigned char nPixel)
{
  unsigned char nByte = 0;

  if(nPixel == 0)
  {
    nByte &= 85;

    if(nColor & 1)
      nByte |= 128;

    if(nColor & 2)
      nByte |= 8;

    if(nColor & 4)
      nByte |= 32;

    if(nColor & 8)
      nByte |= 2;
  }
  else
  {
    nByte &= 170;

    if(nColor & 1)
      nByte |= 64;

    if(nColor & 2)
      nByte |= 4;

    if(nColor & 4)
      nByte |= 16;

    if(nColor & 8)
      nByte |= 1;
  }

  return nByte;
}

unsigned char *GetLineAddress(unsigned char nLine)
{
  return (unsigned char *)0xC000 + ((nLine / 8) * 80) + ((nLine % 8) * 2048);
}

////////////////////////////////////////////////////////////////////////
unsigned char char1,char2,char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////

struct _tStar
{
  int nX;
  int nY;
  int nZ;
  int nVelocity;
  unsigned char nColor;
  unsigned char *pLastAddress;
};

#define STARS_NUM 25
struct _tStar aStars[STARS_NUM];

#define MAX_Z 64
#define ORIGIN_X 80
#define ORIGIN_Y 100

void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned char nStar = 0;
  struct _tStar *pStar = NULL;
  int nScreenX = 0;
  int nScreenY = 0;

  memset(aStars, 0, sizeof(aStars));
  
  //SCR_SET_MODE 0
  __asm
    ld a, #0
    call #0xBC0E
  __endasm;

  //PALETE
  __asm
    ld a, #0
    ld b, #0 ;black
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #1
    ld b, #12 ;Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #2
    ld b, #25 ;Pastel Yellow    
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #3
    ld b, #24 ;Bright Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;

  //Init
  for(nStar = 0; nStar < STARS_NUM; nStar++)
  {
    aStars[nStar].nX = rand() % 3200 - 1600;
    aStars[nStar].nY = rand() % 4000 - 2000;
    aStars[nStar].nZ = rand() % MAX_Z;
    aStars[nStar].nColor = GetMode0PixelColorByte(1 + rand() % 3, 0);
    aStars[nStar].nVelocity = 1 + rand() % 5;
    aStars[nStar].pLastAddress = NULL;
  }

  nTimeLast = GetTime();

  while(1)
  {
    for(nStar = 0; nStar < STARS_NUM; nStar++)
    {
      pStar = &aStars[nStar];

      //delete star
      if(pStar->pLastAddress != NULL)
        *pStar->pLastAddress = 0;

      //move star
      pStar->nZ -= pStar->nVelocity;
      
      //paint star
      nScreenX = ORIGIN_X + pStar->nX / (pStar->nZ > 0 ? pStar->nZ : 1);
      nScreenY = ORIGIN_Y + pStar->nY / (pStar->nZ > 0 ? pStar->nZ : 1);

      if(nScreenX >= 0 && nScreenX < 160 && nScreenY >= 0 && nScreenY < 200)
      {
        pStar->pLastAddress = GetLineAddress(nScreenY) + (nScreenX / 2);
        *pStar->pLastAddress = pStar->nColor;
      }
      else
        pStar->nZ = MAX_Z;

      if(pStar->nZ <= 0)
        pStar->nZ = MAX_Z;

      //printf("%d,%d (%d,%d,%d)\n\r", nScreenX, nScreenY, pStar->nX, pStar->nY, pStar->nZ);
    }

    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      //TXT SET CURSOR 0,0
      __asm
        ld h, #1
        ld l, #1
        call #0xBB75
      __endasm;

      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
  }
}
////////////////////////////////////////////////////////////////////////

Si lo ejecutamos en un emulador veríamos algo similar a esto (mucho más fluido en el emulador):

gifff

Como se puede ver, conseguimos unos 24 frames por segundo con 25 estrellas... la verdad es que se queda un poco corto. Como ya vimos en el tutorial anterior, lo mejor es precalcular todo lo que podamos. En este caso podemos precalcular absolutamente todo, cada estrella como máximo tendría 64 posiciones, podemos precalcularlas todas para evitar hacer las operaciones y dedicarnos únicamente a borrar/pintar. Así quedaría el código modificado para precalcular todo:

////////////////////////////////////////////////////////////////////////
// 3Dstars02.c
// 3D Star Field
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned char GetMode0PixelColorByte(unsigned char nColor, unsigned char nPixel)
{
  unsigned char nByte = 0;

  if(nPixel == 0)
  {
    nByte &= 85;

    if(nColor & 1)
      nByte |= 128;

    if(nColor & 2)
      nByte |= 8;

    if(nColor & 4)
      nByte |= 32;

    if(nColor & 8)
      nByte |= 2;
  }
  else
  {
    nByte &= 170;

    if(nColor & 1)
      nByte |= 64;

    if(nColor & 2)
      nByte |= 4;

    if(nColor & 4)
      nByte |= 16;

    if(nColor & 8)
      nByte |= 1;
  }

  return nByte;
}

unsigned char *GetLineAddress(unsigned char nLine)
{
  return (unsigned char *)0xC000 + ((nLine / 8) * 80) + ((nLine % 8) * 2048);
}

////////////////////////////////////////////////////////////////////////
unsigned char char1,char2,char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////

#define MAX_Z 64
#define ORIGIN_X 80
#define ORIGIN_Y 100

struct _tStar
{
  unsigned char nColor;
  unsigned char nActualAddress;
  unsigned char *aAddress[MAX_Z + 1];
};

#define STARS_NUM 25
struct _tStar aStars[STARS_NUM];

void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned char nStar = 0;
  unsigned char nAddress = 0;
  struct _tStar *pStar = NULL;
  int nScreenX = 0;
  int nScreenY = 0;

  memset(aStars, 0, sizeof(aStars));
  
  //SCR_SET_MODE 0
  __asm
    ld a, #0
    call #0xBC0E
  __endasm;

  //PALETE
  __asm
    ld a, #0
    ld b, #0 ;black
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #1
    ld b, #12 ;Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #2
    ld b, #25 ;Pastel Yellow    
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #3
    ld b, #24 ;Bright Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;

  //Init
  memset(aStars, 0, sizeof(aStars));

  for(nStar = 0; nStar < STARS_NUM; nStar++)
  {
    int nX = rand() % 3200 - 1600;
    int nY = rand() % 4000 - 2000;
    int nZ = MAX_Z;
    int nVelocity = 1 + rand() % 5;

    pStar = &aStars[nStar];
    pStar->nColor = GetMode0PixelColorByte(1 + rand() % 3, 0);
    
    for(nAddress = 0; nAddress < MAX_Z; nAddress++)
    {
      nScreenX = ORIGIN_X + nX / (nZ > 0 ? nZ : 1);
      nScreenY = ORIGIN_Y + nY / (nZ > 0 ? nZ : 1);

      if(nScreenX >= 0 && nScreenX < 160 && nScreenY >= 0 && nScreenY < 200)
      {
        pStar->aAddress[nAddress] = GetLineAddress(nScreenY) + (nScreenX / 2);
      }
      else
      {
        break;
      }

      nZ -= nVelocity;

      if(nZ < 0)
        break;
    }

    pStar->nActualAddress = rand() % nAddress;
  }

  nTimeLast = GetTime();

  while(1)
  {
    for(nStar = 0; nStar < STARS_NUM; nStar++)
    {
      pStar = &aStars[nStar];

      //delete star
      *pStar->aAddress[pStar->nActualAddress] = 0;

      //move star
      pStar->nActualAddress++;

      if(pStar->aAddress[pStar->nActualAddress] == NULL)
        pStar->nActualAddress = 0;
      
      //paint star
      *pStar->aAddress[pStar->nActualAddress] = pStar->nColor;
    }

    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      //TXT SET CURSOR 0,0
      __asm
        ld h, #1
        ld l, #1
        call #0xBB75
      __endasm;

      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
  }
}
////////////////////////////////////////////////////////////////////////

Con estas optimizaciones, pasaríamos de 24 frames por segundo a 81... Como 25 estrellas son pocas vamos a generar una versión final del código, eliminando el calculo y carteles de frames por segundo y aumentando el número de estrellas de 25 a 125, para que el efecto quede más bonito. Este sería el código fuente final:

////////////////////////////////////////////////////////////////////////
// 3Dstars03.c
// 3D Star Field
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned char GetMode0PixelColorByte(unsigned char nColor, unsigned char nPixel)
{
  unsigned char nByte = 0;

  if(nPixel == 0)
  {
    nByte &= 85;

    if(nColor & 1)
      nByte |= 128;

    if(nColor & 2)
      nByte |= 8;

    if(nColor & 4)
      nByte |= 32;

    if(nColor & 8)
      nByte |= 2;
  }
  else
  {
    nByte &= 170;

    if(nColor & 1)
      nByte |= 64;

    if(nColor & 2)
      nByte |= 4;

    if(nColor & 4)
      nByte |= 16;

    if(nColor & 8)
      nByte |= 1;
  }

  return nByte;
}

unsigned char *GetLineAddress(unsigned char nLine)
{
  return (unsigned char *)0xC000 + ((nLine / 8) * 80) + ((nLine % 8) * 2048);
}

#define MAX_Z 64
#define ORIGIN_X 80
#define ORIGIN_Y 100

struct _tStar
{
  unsigned char nColor;
  unsigned char nActualAddress;
  unsigned char *aAddress[MAX_Z + 1];
};

#define STARS_NUM 125
struct _tStar aStars[STARS_NUM];

void main()
{
  unsigned char nStar = 0;
  unsigned char nAddress = 0;
  struct _tStar *pStar = NULL;
  int nScreenX = 0;
  int nScreenY = 0;

  memset(aStars, 0, sizeof(aStars));
  
  //SCR_SET_MODE 0
  __asm
    ld a, #0
    call #0xBC0E
  __endasm;

  //PALETE
  __asm
    ld a, #0
    ld b, #0 ;black
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #1
    ld b, #12 ;Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #2
    ld b, #25 ;Pastel Yellow    
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #3
    ld b, #24 ;Bright Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;

  //Init
  memset(aStars, 0, sizeof(aStars));

  for(nStar = 0; nStar < STARS_NUM; nStar++)
  {
    int nX = rand() % 3200 - 1600;
    int nY = rand() % 4000 - 2000;
    int nZ = MAX_Z;
    int nVelocity = 1 + rand() % 5;

    pStar = &aStars[nStar];
    pStar->nColor = GetMode0PixelColorByte(1 + rand() % 3, 0);
    
    for(nAddress = 0; nAddress < MAX_Z; nAddress++)
    {
      nScreenX = ORIGIN_X + nX / (nZ > 0 ? nZ : 1);
      nScreenY = ORIGIN_Y + nY / (nZ > 0 ? nZ : 1);

      if(nScreenX >= 0 && nScreenX < 160 && nScreenY >= 0 && nScreenY < 200)
      {
        pStar->aAddress[nAddress] = GetLineAddress(nScreenY) + (nScreenX / 2);
      }
      else
      {
        break;
      }

      nZ -= nVelocity;

      if(nZ < 0)
        break;
    }

    pStar->nActualAddress = rand() % nAddress;
  }

  while(1)
  {
    for(nStar = 0; nStar < STARS_NUM; nStar++)
    {
      pStar = &aStars[nStar];

      //delete star
      *pStar->aAddress[pStar->nActualAddress] = 0;

      //move star
      pStar->nActualAddress++;

      if(pStar->aAddress[pStar->nActualAddress] == NULL)
        pStar->nActualAddress = 0;
      
      //paint star
      *pStar->aAddress[pStar->nActualAddress] = pStar->nColor;
    }
  }
}
////////////////////////////////////////////////////////////////////////

Si lo ejecutamos en el emulador, veríamos algo similar a esto (mucho más fluido y rápido en el emulador):

giffff

Podéis bajar un zip con todos ficheros (código fuente, bat's para compilar, binarios y dsk's) aquí: Campo_de_estrellas_3D.zip

 

www.CPCMania.com 2012