Как претворить в жизнь идею компьютерной игры? Приходилось ли вам, играя в свою любимую игру, мечтать о том, как можно было бы ее улучшить

Вид материалаДокументы

Содержание


Третье измерение
Подобный материал:
1   ...   4   5   6   7   8   9   10   11   ...   37
Листинг 5.11. Структура спрайта с полями для анимации.

typedef struct sprite_typ

{

int x,y; // текущая позиция спрайта

int x_old, y_old; // предыдущая позиция спрайта

int width,height; // размеры спрайта

int anim_clock; // время анимации

int anim_speed; // скорость анимации

int motion_speed; // скорость движения

int motion_clock; // время движения

char far *frames [MAX_SPRITE_FRAMES] ; // массив указателей

//на кадры

int curr_frame; // отображаемый кадр

int num_frames; // общее число кадров

int state; // статус спрайта

char far *background; // фон под спрайтом

}sprite, *sprite_ptr;

Структура спрайта имеет поля для сохранения позиции и размеров образа и несколько других элементов. Сейчас мы уже готовы написать функцию для работы со спрайтами.

Прежде всего мы должны извлечь битовую карту из PCX-файла и поместить ее в массив, хранящий образы спрайта. Если вы помните, я создал файл в формате PCX (CHARPLATE.PCX), в который вы можете дорисовать свои картинки и героев. Функция, извлекающая битовые карты из РСХ-образа подразумевает, что вы создали свои образы с помощью этого файла. Программа из Листинга 5.12 позволяет перемещать спрайт, который вы хотите изменить, в указанные координаты.

Листинг 5.12. Функция извлечения спрайта из загруженного PCXфайла.

void PCX_Grap_Bitmap(pcx_picture_ptr image, sprite_ptr sprite, int sprite_franie, int grab_x, int grab_y)

{

// функция выделяет одно изображение из буфера в который

// загружен PCX-файл

// функция исходит из предположения, что в действительности массив

// пикселей размером 320х200 разделен на отдельные изображения

// размером 24х24 пикселя

int x_off,y_off, х,у, index;

char far *sprite_data;

// вначале выделяем память для хранения спрайта в структуре спрайта

sprite->frames[sprite_frame] = (char far *)malloc(SPRITE_WIDTH * SPRITE_HEIGHT);

// создаем альтернативный указатель на эту область памяти

// для ускорения доступа

sprite_data = sprite->frames[sprite_frame];

// теперь перемещаем битовый образ спрайта из области PCX-файла

// в выделенную память

// мы должны выбрать, какой именно спрайт мы копируем

// помните, что в действительности файл представляет собой массив.

// 12х8 элементов, каждый из которых имеет размер 24х24 пикселя.

// Индекс (0,0) соответствует верхнему левому углу спрайта,

// (11,7) - нижнему правому

х_off = 25 * grab_x + 1;

у_off = 25 * grab_y + 1;

// вычисляем начальный адрес

y_off =y_off * 320;

for (y=0; y
{

for (x=0; x
{

// получить очередной байт текущей строки и поместить его

//в следующую позицию буфера спрайта

sprite_data[у*24 + х] = image->buffer[y_off + x_off + х];

} // конец копирования строки

// перейти к следующей строке

y_off+=320;

} // конец копирования

// инкрементировать счетчик кадров

sprite->num_frames++;

} // конец функции

Эта функция по указателю на спрайт определяет его расположение в загруженном файле. Далее она выделяет память для хранения образа и инициализирует структуру данных (я решил делать спрайты размером 24 на 24 пикселя, но вам ничто не мешает изготавливать любые другие спрайты). Теперь, когда у нас подготовлены образы, следующим шагом будет их отображение на экране монитора. Для этого нам надо:
  • Вычислить начальную позицию спрайта согласно его координатам (х,у);
  • Преобразовать полученные координаты в адрес видеобуфера;
  • Переместить байты изображения в видеобуфер.

Для рисования спрайта мы должны выполнить все операции с текущим кадром анимации. Код в Листинге 5.13 делает все перечисленное.

Листинг 5.13. Функция рисования спрайта.

void DrawSprite (sprite_ptr sprite)

{

// функция, рисующая спрайт на экране строка за строкой,

// очень быстро. Вместо умножения используется сдвиг

char far *work_sprite;

int work_offset=0,offset,x,у;

unsigned char data;

// создаем альтернативный указатель на спрайт для ускорения доступа

work_sprite = sprite->frames[sprite->curr frame];

// вычислить смещение спрайта в видеобуфере

offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x;

for (y=0; y
{

for (x=0; x
{

// Проверка на "прозрачный" пиксель (с кодом 0). Если пиксель

// "непрозрачный" - выводим его на экран.

if ((data=work_sprite[work_offset+x])) video_buffer[offset+xj = data;

} // конец вывода строки

// перейти к следующему ряду пикселей в видеобуфере

//в буфере спрайта

offset += SCREEN_WIDTH;

work_offset += SPRITE_WIDTH;

} // коней вывода спрайта

} // конец функции

Эта функция работает примерно так же, как и Plot_Pixel_Fast из Листинга 5.5. Сначала вычисляется стартовый адрес расположения спрайта, а затем все его байты строка за строкой переписываются в видеобуфер.

Следует учесть, что здесь потребуется некоторая оптимизация. Почему бы не использовать функцию memcpy, чтобы копировать всю строку разом (а всего 24 строки)? Однако этого сделать нельзя, так как при выводе спрайта нам необходимо применить технику, использующую "прозрачные" пиксели. Что это дает? Взгляните на рисунок 5.12.

Спрайт слева выведен вместе с фоном, а при рисовании правого спрайта использовалась техника «прозрачных» пикселей. При этом пиксели с цветом фона (черный, имеющий код 0) пропускались, а прорисовывались только данные непосредственного изображения. Это и создает эффект «прозрачности» фона.

Следующая функция, которую я собираюсь вам предложить, будет сохранять фон перед тем, как мы выведем спрайт на экран. Помните, когда мы что-то записываем в видеобуфер, данные или образ, находящийся там, обязательно теряются. Поэтому мы и должны сохранять фон под спрайтом прежде чем поместим его в видеобуфер, чтобы позже восстановить прежний вид экрана. Код в Листинге 5.14 именно это и делает.

Листинг 5.14. Сохранение фона под спрайтом.

void Behind_Sprite(sprite_ptr sprite)

{ // функция сохраняет область видеопамяти, в которую будет

// выводиться спрайт

char far *work_back;

in work_offset=0,offset,y;

// создаем альтернативный указатель для ускорения доступа

work_back = sprite->background;

// вычисляем смещение в видеобуфере

offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x;

for (y=0; y
{

// копируем строку видеопамяти в буфер

_fmemcpy((char far *)&work_back[work_offset], (char far *)&video_buffer[offset], SPRITE_WIDTH);

// переходим к следующей строке

offset += SCREEN_WIDTH;

work_offset += SPRITE_WIDTH;

} // конец цикла for

} // конец функции

Функция Behind_Sprite считывает матрицу размером 24х24, где должен находится спрайт. Данные после считывания находятся в поле background структуры спрайта. Это поле является указателем на область памяти, где находится спрайт.

Собственно, это все, что нам нужно для создания и перемещения маленьких образов по экрану. Для анимации мы должны изменять поле curr_frame в структуре спрайта, перед тем, как его начать рисовать. Мы обсудим процесс анимации в этой главе, но, я думаю, вы и сами догадываетесь, как это сделать: надо стереть спрайт, передвинуть его, снова нарисовать и т. д. Вот и все.

Отображение текста

Одна из самых последних тем, понимание которой очень важно для создания видеоигр, это печать текста. Если вы пропустите этот раздел, не прочитав ни слова, то очень ошибетесь, потому как не существует игр, в которых не встречалось бы никаких надписей.

Поскольку наши игры будут работать в графическом режиме, то единственный способ напечатать текст на экране - это выводить его по точкам. Мы можем нарисовать все 128 символов и загружать их из PCX-файла, но боюсь, что это похоронит все наши дальнейшие планы. Я бы лучше использовал битовые образы внутреннего набора символов, скрытые в памяти ПК. Если бы мы знали их местоположение, то смогли бы их использовать для вывода символов и надписей любого цвета и размера. Не верите? Ладно, я знаю, как это сделать, и сейчас научу вас.

Данные для таблицы символов ROM размером 8х8 начинаются с адреса F000:FA6E. Символы располагаются в ASCII-последоватедьности, то есть описание символа А находится на 65-м месте и т. д. Каждый символ состоит из восьми байт, где каждый байт представляет собой одну строку битовой карты символа. Мы можем умножить 65 на 8, чтобы найти смещение первого байта данных в наборе символов. Это смещение затем складывается с базовым адресом FOOO:FA6E, и следующие восемь байт представляют каждую из строк символа. Давайте посмотрим на рисунок 5.13 для большей ясности.

Единственная проблема режиме в 13h, которая у нас возникает, заключается в том, что для каждого пикселя требуется не один бит, а целый байт, в то время как каждая строка любого символа описывается байтом! И ничего с этим не сделать. Мы можем только окрашивать символы определенным цветом.

Алгоритм выглядит примерно так:
  • Получить следующую строку данных символа (следующий байт);
  • Нарисовать по горизонтали восемь точек, где пиксели с 0 по 7 установлены либо в 1, либо в 0, в зависимости от значения битов 0 - 7 в байте данных;


  • Проделать то же самое для каждой строки данных символа (байта).

Листинг 5.15 содержит код для изображения символа любого цвета в любой позиции на экране.

Листинг 5.15. Отображение символа.

void Blit_Char(int xc,inf ус,char c,int color)

{

// Функция использует описание символов 8х8 байт, хранящееся в ПЗУ

// для изображения символа на экране. Хитрость заключена в том, что

// в этом описании каждая экранная точка представлена одним битом.

int offset,x,у;

unsigned char data;

char far *work_char;

unsigned char bit_mask = 0х80;

// вычисляем начальное смещение символа в ROM

work_char= rom_char_set + с * CHAR_HEIGHT;

// вычисляем смещение символа в видеобуфере

offset = (ус << 8) + (ус << 6) + хс;

for (y=0; y
{

// сбросить битовую маску

bit_mask = 0х80;

for (x=0; x
{

// если бит равен 1, рисуем пиксель

if ((*work_char & bit_niask))

video_buffer[offset+x] = color;

bit_mask = (bit_mask>>l);

} // конец обработки строки

// переходим к следующей строке

offset += SCREEN_WIDTH;

work_char++;

} // конец рисования символа

} // конец функции


Теперь и печать текстов не представляет труда. Нужно только выдерживать пространство между символами и печатать их из заданной строки один за другим, пока не встретится конец строки (NULL). В демонстрационной программе в конце этой главы мы используем подобную функцию.

Теперь давайте поговорим о методах анимации и рендеринга, которые применяются технике программирования игр.

Дублирующее буферизирование

Дублирующее буферизирование — это метод, позволяющий избежать мерцания, которое может возникать при перемещении объектов. Когда функции, которые мы пишем, рисуют спрайты па экране, то они это делают, не учитывая статус состояния VGA-карты, то есть в них отсутствует синхронизация с дисплеем. Существует два способа уменьшить возможное мигание.

Первый метод называется дублирующим буферизированием. При этом экранное изображение формируется в памяти, а затем копируется в видеобуфер. Это позволяет минимизировать перемещение маленьких участков в видеопамяти, поскольку сразу перемещается один блок. Пример дублирующего буферизирования изображен на рисунке 5.14.

Переключение страниц - это видоизмененное дублирующее буферизирование. При использовании этого метода две видеостраницы находятся во взаимосвязи. Когда одна из них воспроизводится на экране, другая перерисовывается. Новая страница затем воспроизводится переключением указателя на нее. Обратите внимание на рисунок 5.15 для осмысления этого метода.

Оба этих метода имеют общий недостаток - уменьшение быстродействия в два раза, но при этом отсутствует мигание и достигается очень высокое качество анимации. Мы обратимся к этому методу еще раз в седьмой главе, «Продвинутая битовая графика и специальные эффекты». Сейчас поговорим об уменьшении мерцания.




Вертикальный обратный ход луча

Образ, рисуемый на экране ЭЛТ (электронно-лучевой трубки) и управляемый картой VGA, образуется в результате взаимодействия следующих факторов:
  • Луч электронов движется, по экрану слева направо и сверху вниз, рисуя картинку;
  • Когда он достигает нижней границы, он вновь возвращается вверх и все начинается сначала.

Рисунок 5.16 показывает это.

  • Чтобы вернуться в исходную позицию, лучу требуется примерно 1/60 секунды. Это идеальное время для обновления видеобуфера. В течение этого периода видеобуфер недоступен VGA-карте. Таким образом, 1/60 секунды — это аппаратно-зависимый параметр.

В седьмой главе, «Продвинутая битовая графика и специальные эффекты», мы узнаем, как «синхронизировать» наши игры с этим сигналом и создать чистые, свободные от мерцания изображения.

Тайминг

Я попытался придумать хорошую демонстрацию перемещения спрайтов и решил, что для этого подойдет маленький городок с ковбоем, который ходит по улице. Не так уж плохо. Я хотел еще, чтобы он время от времени стрелял, но позже решил не усложнять дело. Для осуществления реалистичной анимации мы должны уделять большое внимание таймингу, то есть задержке между выводом кадров. Если образ имеет 10 анимационных кадров и мы будем их менять слишком быстро, то персонаж станет похож на лунатика. Поэтому мы должны иметь в программе счетчики времени, чтобы наши персонажи выполняли определенные действия с заданной скоростью. В нашем случае мы используем четыре переменные для сохранения счетчиков движения и анимации:
  • anim_clock
  • anim_speed
  • motion_clock
  • motion_speed

Переменные скорости - это константы, а переменные времени обновляются при каждом проходе через главный цикл. Когда переменная времени оказывается больше переменной скорости, то мы осуществляем какое-либо действие: перемещаем спрайт или сменяем кадр. Переменные времени дри этом обнуляются. Это позволяет нам сделать перемещение спрайта по экрану и смену фаз движения независимым друг от друга и от быстродействия машины. Мы еще вернемся к этому в седьмой главе.

Игра Tombstone

Мы начали с того, что вообще ничего не знали о VGA-карте. Сейчас мы знаем очень много. Поэтому я думаю, что было бы лучше, если бы вы написали маленькую двухмерную игру для закрепления пройденного материала. Чтобы вам в этом помочь, я написал демонстрацию и назвал ее Tombstone.

В этой демонстрации маленький ковбой ходит по городу с различной скоростью. У вас есть все инструменты, чтобы «дать» ему пистолет и "научить" стрелять. В PCX-файле на дискете вы найдете для этого все необходимые картинки. Вся программа за исключением функции Set_Mode() дана в Листин­ге 5.16. Прилинкуйте Set_Mode(), когда будете создавать исполняемый файл.

Листинг 5.16. Tombstone (TOMB.С).

// ВКЛЮЧАЕМЫЕ ФАЙЛЫ /////////////////////////////////////////////

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

// ОПРЕДЕЛЕНИЯ //////////////////////////////////////////////////

#define ROM_CHAR_SET_SEG 0xF000 // сегмент описания символов в ПЗУ

#define ROM_CHAR_SET_OFF 0xFA6E // смещение, соответствующее

// описанию первого символа

#define VGA256 0х13

#define TEXT_MODE 0х03

#define PALETTE_MASK ОхЗC6

#define PALETTE_REGISTER_RD 0x3C7

#define PALETTE_REGISTER_WR 0x3C8

#define PALETTE_DATA 0x3C9

#define SCREEN_WIDTH (unsigned int)320

#define SCREEN_HEIGHT (unsigned int)200

#define CHAR_WIDTH 8

#define CHAR_HEIGHT 8

#define SPRITE_MIDTH 24

#define SPRITE_HEIGHT 24

#define MAX_SPRITE_FRAMES 16

#define SPRITE_DEAD 0

#define sprite_alive 1

#define SPRITE_DYING 2

// СТРУКТУРЫ ДАННЫХ ////////////////////////////////////////

typedef struct RGB_color_typ

{

unsigned char red; // красная составляющая цвета (0-63)

unsigned char green; // зеленая составляющая цвета (0-63)

unsigned char blue; // синяя составляющая цвета (0-63)

} RGB_color, *RGB_color_ptr;


typedef struct pcx_header_typ

{ char manufacturer;

char version;

char encoding;

char bits_per_pixel;

int x,y;

int width,height;

int horz_res;

int vert_res;

char ega_palette[46];

char reserved;

char num_color_planes;

int bytes_per_line;

int palette_type;

char padding[58];

} pcx_header, *pcx_header_ptr;

typedef struct pcx_picture_typ

{

pcx_header header;

RGB_color palette[256];

char far *buffer;

} pcx_picture, *pcx_picture_ptr;

typedef struct sprite_typ

{

int x,y; // текущая позиция спрайта

int x_old,y_old; // предыдущая позиция спрайта

int width,height; // размеры спрайта

int anim_clock; // время анимации

int anim_speed; // скорость анимации

int motion_speed; // скорость движения

int motion_clock; // время движения

char far *frames[MAX_SPRITE__FRAMES]; // массив указателей

// на образы

int curr_frame; // отображаемый фрейм

int num_frames; // общее число фреймов

int state; // статус спрайта

char far *background; // фон под спрайтом

}sprite, *sprite_ptr;

// ВНЕШНИЕ ФУНКЦИИ /////////////////////////////////

extern Set_Mode(int mode);

// ПРОТОТИПЫ ///////////////////////////////////////

void Set_Palette_Register(int index, RGB_color_ptr color);

void Plot_Pixel_Fast(int x,int y,unsigned char color);

void PCX_Init(pcx_picture *image);

void PCX_Delete(pcx_picture *image);

void PCX_Load(char *filename, pcx_picture_ptr image, int enable_palette) ;

void PCX_Show_Buffer(pcx_picture_ptr image);

// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ //////////////////////////////

unsigned char far *video_buffer = (char far *) 0xA0000000L;

unsigned int far *video_buffer_w = (int far *)0xA0000000L;

unsigned char far *rom_char_set = (char far *)0xF000FA6EL;

// ФУНКЦИИ ////////////////////////////////////////////

void Blit_Char(int xc,int yc,char c,int color) {

// эта функция отображает символ на экране

// используя описание

// символов размером 8х8 точек, хранящееся в ПЗУ

int offset,х,у;

unsigned char data;

char far *work_char;

unsigned char bit_mask = 0х80;

// вычисляем смещение описания символа в ПЗУ

work_char = rom_charset + с * CHAR_HEIGHT;

// вычисляем смещение символа в видеобуфере

offset = (ус << 8} + (ус << 6) + хс;

for (у=0; y
{

// сбросить битовую маску

bit_mask = 0х80;

for (x=0; x
{ // если бит равен 1, рисуем пиксель

if ((*work_char & bit_mask))

video_buffer[offset+x] = color;

// сдвигаем маску

bit_mask = (bit_mask>>1);

} // конец отрисовки строки

// переходим к следующей строке

offset += SCREEN_WIDTH;

work_char++;

} // конец рисования

} // конец функции

//////////////////////////////////////////////////////////

void Blit_String(int x,int y,int color, char *string)

{ // функция отображает на экране передаваемую строку символов

// Расстояние между символами строки постоянно

// Для отображения символов вызывается функция blit_char

int index;

for (index=0; string[index]!=0; index++)

{

Blit_Char(x+(index<<3) ,y, string[index],color);

} // конец цикла for

} // конец функции

///////////////////////////////////////////////////////

void Delay(int t)

{

float x = 1;

while(t—>0)

x=cos(x);

} // конец функции

///////////////////////////////////////////////////////

void Set_Palette_Register(int index, RGB_color_ptr color) {

// функция устанавливает элемент таблицы цветов задаваемый

// параметром index. Значения компонент цвета задаются полями

// структуры color.

// указываем VGA-карте, что мы будем изменять регистр палитры

_outp(PALETTE_MASK,Oxff);

// указываем номер изменяемого регистра

_outp(PALETTE_REGISTER_WR, index);

// теперь меняем значения компонентов. Каждый раз используется

// один и тот же порт

_outp(PALETTE_DATA,color->red);

_outp(PALETTE_DATA,color->green);

_outp(PALETTE_DATA,color->blue);

} // конец функции

///////////////////////////////////////////////////////

void PCX_Init(pcx_picture_ptr image)

{

// функция выделяет память для загрузки PCX-файла

if ((image->buffer = (char far *)malloc (SCREEN_WIDTH * SCREEN_HEIGHT +1)));

printf("\ncouldn't allocate screen buffer");

} // конец функции

///////////////////////////////////////////////////////

void Plot_Pixel_Fast(int x,int y,unsigned char color)

{

// функция отображает на экране точку заданного цвета

// вместо умножения используется сдвиг

// пользуемся тем, что 320*у=256*у+64*у=у<<8+у<<6

video_buffer[ ((у<<8) + (у<<6)) + х] = color;

} // конец функции ///////////////////////////////////////////////////////////////////////////

void PCX_Delete(pcx_picture_ptr image)

{ // функция освобождает память, выделенную для загрузки PCX-файла

_ffree (image->buffer);

} // конец функции

//////////////////////////////////////////////////////////

void PCX_Load(char *filename, pcx_picture_ptr image, int enable_palette}

{ // функция загружает PCX-файл и заполняет поля структуры

// pcx_picture, включая изображение (после декомпрессии),

// заголовок и палитру

FILE *fp;

int num_bytes,index;

long count;

unsigned char data;

char far *temp_buffer;

// открыть файл

fp = fopen(filename,"rb");

// загрузить заголовок

temp_buffer = (char far *)image;

for (index=0; index<128; index++)

{

temp_buffer[index] = getc(fp);

} // конец цикла for

// загрузить данные и декодировать их в буфере

count=0;

while(count<=SCREEN_WIDTH * SCREEN_HEIGHT)

{ // получить первую часть данных

data = getc(fp);

// это RLE?

if (data>=192 && data<=255)

{ // сколько байт сжато?

num_bytes = data-192;

// получить значение цвета для сжатых данных

data = getc(fp);

// заполняем буфер полученным цветом

while(num_bytes-->0)

{

image->buffer[count++] = data;

} // конец цикла while

} // конец обработки сжатых данных

else

{

// поместить значение цвета в очередную позицию

image->buffer[count++] = data;

} // конец обработки несжатых данных

} // конец цикла while

// перейти в позицию, не доходя 768 байт от конца файла

fseek(fp,-768L,SEEK_END) ;

// загрузить палигру

for (index=0; index<256; index++)

{

// красная составляющая

image->palette[index].red = (getc(fp) >> 2);

// зеленая составляющая

image->palette[index].green = (getc(fp) >> 2);

// синяя составляющая

image->palette[index].blue = (getc(fp) >> 2) ;

} // конец цикла for

fclose(fp);

// если установлен флаг enable_palette, установить новую палитру

if (enable_palette)

{

for (index=0; index<256; index++)

{

Set_Palette_Register(index,

(RGB_color_ptr)&image->palette[index]);

} // конец цикла for


} // конец установки палитры

} // конец функции


//////////////////////////////////////////

void PCX_Show_Buffer (pcx_picture_ptr image)

{ // функция копирует буфер, содержащий изображение из PCX-файла,

// в видеопамять

_fmemcpy(char far *)video_buffer,

(char far *) image->buffer,SCREEN_WIDTH*SCREEN__HEIGHT);

} // конец функции

////////////////////////////////////////////////////

void Sprite_Init(sprite_ptr sprite, int x,int y, int ac, int as,int mc,int ms)

{

// функция инициализирует спрайт

int index;

sprite->x = x;

sprite->y = у;

sprite->x_old = x;

sprite->y_old = у;

sprite->width = SPRITE_WIDTH;

sprite->height = SPRITE_HEIGHT;

sprite->anim_clock = ac;

sprite->anim_speed = as;

sprite->motion_clock = me;

sprite->motion_speed = ms;

sprite->curr frame = 0;

sprite->state = SPRITE_DEAD;

sprite->num_frames = 0;

sprite->background = (char far *)malloc (SPRITE_WIDTH * SPRITE_HEIGHT+1);

// устанавливаем все указатели в значение NULL

for (index=0; indexframes[index] = NULL;

} // конец функции

////////////////////////////////////////////////////////

void Sprite_Delete(sprite_ptr sprite)

{ // функция освобождает всю связанную со спрайтом память

int index;

_ffree(sprite->background) ;

// освобождаем память, выделенную под кадры анимации

for (index=0; indexframes(index]);

} // конец функции

///////////////////////////////////////////////////////

void PCX_Grap_Bitmap (pcx_picture_ptr image, sprite_ptr sprite, int sprite_frame, int grab_x, int grab_y)

{

// функция выделяет один кадр из буфера PCX-файла.

// Предполагается, что изображение размером 320х200 пикселей

// в действительности представляет собой массив 12х8 изображений

// каждое размерностью по 24х24 пикселя

int x_off,y_off, x,y, index;

char far *sprite_data;

//вначале выделяем память для размещения спрайта

sprite->frames[sprite_frame] = (char far *)malloc (SPRITE_WIDTH * SPRITE_HEIGHT);

// создаем альтернативный указатель для ускорения доступа

sprite_data = sprite->frames[sprite_frame];

// загружаем битовый образ в выделенную область памяти

// вначале вычисляем, какое из изображений копировать.

// Помните: в действительности PCX-файл представляет собой массив

// из отдельных элементов размером 24х24 пикселя.

// Индекс (0,0) соответствует левому верхнему изображению,

// (11,7) - правому нижнему.

x_off = 25 * grab_x + 1;

y_off = 25 * grab_y + 1;

// вычисляем начальный адрес

y_off = y_off * 320;

for (y=0; y
{

for (x=0; x
{

// берем очередной байт и помещаем его в текущую позицию буфера

sprite_data[y*24 + х] = image->buffer [y_off + х_off + х];

} // конец копирования строки

// переходим к следующей строке y_off+=320;

} // конец копирования

// увеличиваем счетчик кадров sprite->num_frames++;

} // конец функции

//////////////////////////////////////

void Behind_Sprite(sprite_ptr sprite)

{

// функция сохраняет содержимое видеопамяти в той области,

// куда будет выведен спрайта

char far *work back;

int work_offset=0,offset,y;

// создаем альтернативный указатель для ускорения доступа

work_back = sprite->background;

// вычисляем смещение в видеопамяти

offset = (sprite->y << 6) + (sprite->y << 6) + sprite->x;

for (y=0; y
{

// копируем очередную строку видеопамяти в буфер

_fmemcpy((char far *)&work_back[work_offset], (char far *)&video_buffer[offset], SPRITE_WIDTH);

// переходим к следующей строке

offset += SCREEN_WIDTH;

work_offset += SPRITE_WIDTH;

} // конец цикла for

} // конец функции

///////////////////////////////////////////

void Erase_Sprite(sprite_ptr sprite)

{ // функция восстанавливает фон, сохраненный перед выводом спрайта

char far *work_back;

int work_offset=0,offset,y;

// создаем альтернативный указатель для ускорения доступа

work_back = sprite->background;

// вычисляем смещение в видеобуфере

offset = (sprite->y_old << 8) + (sprite->y_old << 6} + sprite->x_old;

for (y=0; y
{

// копируем в видеопамять очередную строку буфера

_fmemcpy((char far *)&video_buffer [offset],(char far *)&work_back[work_offset], SPRITE_WIDTH);

// перейти к следующей строке

offset += SCREEN_WIDTH;

work_offset += SPRITE_WIDTH;

} // конец цикла for

} // конец функции

///////////////////////////////////////////////////////

void Draw_Sprite(sprite_ptr sprite)

{

// функция, рисующая спрайт на экране вместо умножения

// используется сдвиг

char far *work sprite;

int work_offset=0,offset,x,y;

unsigned char data;

work sprite = sprite->frames[sprite->curr_frame];

// вычислить смещение спрайта в видеобуфере

offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x;

for (y=0; y
{

for (x=0; X
{

// проверяем, не является ли пиксель "прозрачным" (с кодом 0),

// если нет - рисуем

if ((data=work_sprite[work_offset+x]))

video_buffer[offset+x] = data;

} // конец вывода строки

// перейти к следующей строке в видеобуфере и буфере спрайта

offset += SCREEN_WIDTH;

work_offset += SPRITE_WIDTH;

} //конец цикла no Y

} // конец функции

// ОСНОВНАЯ ПРОГРАММА /////////////////////////////////////////

void main(void)

{

long index,redraw;

RGB_color color;

int frame_dir = 1;

pcx_picture town, cowboys;

sprite cowboy;

// установить видеорежим 320х200х256

Set_Mode(VGA256);

// установить глобальные указатели на область памяти экрана

Set_Screen_Pointers();

// загрузить фон

PCX_Init((pcx_picture_ptr)&town) ;

PCX_Load( "town. pox", (pcx_picture_ptr)&town, 1) ;

PCX_Show_Buffer((pcx_picture_ptr)&town);

PCX_Delete((pcx_picture_ptr)&town);

// вывести на экран заставку игры

Blit_String(128, 24,50, "TOMBSTONE");

// загрузить образы

PCX_Init((pcx_picture_ptr)&cowboys) ;

PCX_Load("cowboys.pcx", (pcx_picture_ptr) &cowboys,0) ;

// извлечь все образы из PCX-файла

Sprite_Init((sprite_ptr)&cowboy,SPRITE_WIDTH,100,0,7,0,3) ;

PCX_Grap_Bitmap ((pcx_picture_ptr) &cowboys, (sprite_ptr) &cowboy, 0, 0, 0);

PCX Grap_Bitmap( (pcx_picture_ptr) &cowboys,

(sprite_ptr)&cowboy,1,1,0) ;

PCX_Grap_Bitmap((pcx_picture_ptr)&cowboys,

(sprite_ptr)&cowboy,2,2, 0) ;

PCX_Grap_Bitmap ((pcx_picture_ptr)&cowboys,

(sprite_ptr)&cowboy,3,1,0) ;

// очистить память, выделенную для загрузки PCX-файла PCX_Delete((pcx_picture_ptr)&cowboys);

Behind_Sprite((sprite_ptr)&cowboy);

Draw_Sprite ((sprite_ptr) &cowboy);

// главный цикл

cowboy.state = SPRITE_ALIVE;

while (!kbhit()) {

redraw = 0; // используется как флаг необходимости перерисовки

if (cowboy.state==SPRITE_ALIVE)

{

//не пора ли менять кадр?

if (++cowboy.anim_clock > cowboy.anim_speed)

{

// сбрасываем счетчик времени

cowboy.anim_clock = 0;

if (++cowboy.curr_frame >= cowboy. num_frames)

{

cowboy.curr_frame = 0;

} // конец обработки достижения последнего кадра

redraw=1;

} // конец смены кадра

// проверяем не пора ли передвигать спрайт

if (++cowboy.motion_clock > cowboy.motion_speed)

{

// переустановить счетчик движения

cowboy.motion clock =0;

// сохранить старую позицию

cowboy.x_old == cowboy.x;

redraw =1;

//передвинуть спрайт

if (++cowboy.x >= SCREEN_WIDTH-2*SPRITE_WIDTH)

{

Erase_Sprite((sprite_ptr)&cowboy);

cowboy.state = SPRITE_DEAD;

redraw = 0;

} // конец обработки достижения последнего кадра

} // конец обработки движения спрайта

} // конец обработки ситуации "ковбой жив"

else

{ // пытаемся "оживить" ковбоя

if (rand()%100 == 0 )

{

cowboy.state = SPRITE_ALIVE;

cowboy.x = SPRITE_WIDTH;

cowboy.curr frame = 0;

cowboy.anim.speed = 3 + rand()%6;

cowboy.motion_speed = 1 + rand()%3;

cowboy.anim_clock = 0;

cowboy.motion_clock = 0;

Behind_Sprite{(sprite_ptr)&cowboy);

}

} // конец процесса "оживления"

// теперь состояние спрайта изменено

if (redraw)

{

// удалить спрайт в старой позиции

Erase_Sprite((sprite_ptr)&cowboy) ;

// сохранить фон в новой позиции

Behind_Sprite((sprite_ptr)&cowboy) ;

// нарисовать спрайт в новой позиции

Draw_Sprite((sprite_ptr)&cowboy);

// обновить старые координаты

cowboy.x_old = cowboy.x;

cowboy.у_old = cowboy.у;

} // конец перерисовки

Delay(1000);

} // конец главного игрового цикла

for(index=0; index <300000;index++,Plot_Pixel_Fast(rand()%320,

rand()%200,0));

//Перейти обратно в текстовый режим

Set_Mode(TEXT_MODE);

}//Конец функции main


Итог

Эта глава, наверное, самая важная в книге. Среди всей информации, которая, может быть, не относится напрямую к играм, вы узнали общую концепцию построения видеоигр, что абсолютно необходимо для их написания. Так что, если вы чего-то не поняли, то остановитесь и прочтите главу вновь (не беспокойтесь, я вас подожду).

Если вы прочитали и поняли в этой главе все, то .должны знать, что:
  • Мы изучили VGA-карту и ее архитектуру;
  • Мы узнали о режиме l3h, который дает лучшее разрешение и наиболее прост для программирования;
  • Мы узнали, как в режиме l3h программировать регистры цвета, рисовать пиксели, загружать PCX-файлы и перемещать битовые образы;
  • Мы поговорили о спрайтах и создали простую библиотеку для работы с ними;
  • Мы затронули довольно сложные вещи, такие как дублирующий буфер и
  • вертикальная синхронизация, о которых более подробно мы поговорим чуть позже;
  • И, наконец, собрав все вместе, мы сделали демонстрационную программу, рисующую маленького ковбоя, который ходит по городу.

До встречи в новой главе.

^ ТРЕТЬЕ ИЗМЕРЕНИЕ

Последние достижения в области электроники и программирования приблизили наши фантазии о трехмерных видеоиграх к осязаемой реальности. Разработчики игр сегодня могут создать трехмерные игры для недорогого ПК, позволяющие игрокам получить ощущения, всего несколько лет назад казавшиеся недоступными,

Говоря по правде, и в прошлом были трехмерные имитаторы полетов и несколько пространственных игр. Но все они были очень медленными и нуждались в мощных компьютерах. Естественно, это резко сужало круг потенциальных игроков. Все эти ограничения заставляли программистов искать пути создания нетрадиционных методов рен-деринга. Эти новые способы были очень просты и имели производительность, еще пару лет назад считавшуюся невозможной.

В данной главе мы сначала обсудим традиционные методы рендеринга для ПК. а затем погрузимся в технику, называемую «отсечение лучей». Этот метод осуществляет рендеринг со скоростью, близкой к световой...

Из этой главы вы узнаете:

  • Что такое трехмерное пространство
  • Как изображаются точки, многоугольники и объекты в трехмерном пространстве;
  • Масштабирование, трансляция и повороты в трехмерном пространстве;
  • Проекции;
  • Геометрическое моделирование;
  • Алгоритм удаления невидимой поверхности;
  • Алгоритм художника;
  • Алгоритм, использующий Z-буфер;
  • Что такое текстуры;
  • Метод трассировки лучей;
  • Алгоритм отсечения лучей;
  • Реализация алгоритма отсечения лучей;
  • Усовершенствование алгоритма отсечения лучей;
  • Освещение, затемнение и цветовая палитра.


Мы начнем наш урок с основных концепций трехмерного пространства, чтобы кое-что вспомнить и понять. Затем мы бросимся вперед, чтобы узнать как создаются игры типа Wolfenstein, DOOM и Terminator Rampage!

Что такое трехмерное пространство

Трехмерное пространство... Звучит как строка из фантастического рассказа... Ну очень похоже. Трехмерное пространство - это просто расширение двухмерной плоскости. Надо сразу отметить, что рендеринг трехмерной графики весьма сложен. Вообще же, сложность рендеринга экспоненциально возрастает с добавлением новых измерений и происходит это оттого, что и сами образы при этом усложняются. Наша задача заключается в том, чтобы понять, как работать с новым измерением. Мы должны все это изучить.

Говоря языком математики, любая точка в трехмерном пространстве описывается с помощью уникального набора трех координат: х, у и z. Как мы уже обсуждали ранее, обычно экран представляется плоскостью Х и Y, а координата z перпендикулярна экрану.

В отличие от плоскости, где х-координата горизонтальна, а у-координата вертикальна, трехмерные системы координат бывают двух типов:
  • Левосторонняя система координат;
  • Правосторонняя система координат.



На рисунке 6.1 показано представление обоих способов отображения трехмерных систем.

В дальнейшем для всех наших рассуждений, примеров и программ мы будем использовать только правостороннюю' систему координат. На то есть две причины:
  • Правосторонняя система удобней в работе, поскольку в ней проще выполняется визуализация;
  • Правосторонняя система распространена как стандарт.

Конечно, вы можете сказать, что экран компьютера — это плоскость, и мы не можем преобразовывать трехмерные образы в двух измерениях. Верно. Вы абсолютно правы. Но у нас есть возможность отобразить их на плоскость. Мы даже можем видеть «тени» объектов. И сделать это позволит проекция. При этом модель выглядит на двухмерном экране так, что у зрителей возникает полное ощущение объемности. Такие игры как DOOM и Wolfenstein трехмерны только в нашем восприятии. Можно сказать, что в них смоделирован особый случай трехмерного пространства, образы которого можно обрабатывать гораздо проще и быстрее.

В любом случае, мы еще к этому вернемся, а теперь давайте поговорим об основных понятиях трехмерного пространства.

Точки, линии, многоугольники и объекты в трехмерном пространстве

Как мы уже видели, точка в трехмерном пространстве имеет три координаты (x,y,z). Этой информации достаточно, чтобы ее однозначно определить в пространстве.Будет логично, если следующим объектом, который мы определим, станет линия. Линией называют отрезок, соединяющий две точки в трехмерном пространстве. Мы можем даже написать структуры данных, определяющие точку и линию.

Листинг 6.1. Определение точки и линии в трехмерном пространстве.

// структура, описывающая точку в трехмерном пространстве

typedef struct point_typ

{

float x,y,z // координаты точки

} point, *point_ptr;

// структура, описывающая линию в трехмерном пространстве

typedef struct line_typ

{

point start, end; // линия задается двумя точками

} line, *line_ptr;


Используя структуры из Листинга 6,1, давайте определим линию, которая начинается в точке (0,0,0) и идет в точку (100,200,300)

line linel;

linel.start.x = 0;

linel.start.у= 0;

linel.start.z = 0;

linel.end.x = 100;

linel.end.у = 200;

linel.end.z = 300;

Теперь мы имеем описание линии. Если мы захотим, то сможем создать трехмерный мир, состоящий из линий и точек, но это будет скучным и тоскливым занятием.

Нам нужен больший уровень абстракции для моделирования объектов, и для этого нам пригодятся многоугольники. Как вы поняли из четвертой главы, многоугольник - это множество вершин, соединенных отрезками прямых. Вершины определяют границы многоугольника. В трехмерном пространстве Многоугольники очень похожи на своих двухмерных собратьев. Попробуем определить трехмерный треугольник. Он может выглядеть так, как это изображено иа рисунке 6.2.



Как вы можете видеть, на листе бумаги весьма несложно представить трехмерный объект. Мы будем использовать для описания «диагональный вид». Позже,мы к этому еще вернемся, а сейчас важно понять идею.

Описать многоугольник довольно просто: мы применим старое определение многоугольника и просто добавим к нему несколько атрибутов для создания новой законченной структуры. В Листинге 6.2 показана такая структура.

Листинг 6.2. Определение трехмерного многоугольника.


// структура, описывающая многоугольник

typedef struct polygon_typ

{

int num_vertices; // число вершин

vertices[MAX VERTICES]; // координаты вершин

int color; // цвет многоугольника

}polygon, *polygon_ptr;

Как можно заметить, в структуре описаны вершины и цвета. Эти составляющие необходимы для правильного отображения. Теперь, когда у нас есть структура, описывающая многоугольник, следующим шагом будет определение объекта на основе многоугольников. На рисунке 6.3 продемонстрирован один из таких объектов.

Теперь мы можем добавить еще один уровень к нашему описанию. Объект - это набор многоугольников. Создадим структуру, которая бы поддерживала эту концепцию:



Листинг 6.3. Описание трехмерного объекта на основе многоугольников.

// структура, описывающая объект

typedef struct object_typ

{

int num_faces; // число граней

polygon faces[max_faces]; // грани, представленные многоугольниками

float xo,yo,zo; // координаты объекта в пространстве

int visible; // виден ли объект на экране?

} object, *object_ptr;

Структура данных в Листинге 6.3 описывает объект, который образован , множеством многоугольников или поверхностей. Используя эти структуры данных и определения, мы можем создать несколько трехмерных объектов: космический корабль, планету и окружающее космическое пространство.

Чтобы поместить объекты в трехмерное пространство, мы должны знать их пространственное расположение. То есть мы должны определить значения хо, уо и zo для каждого предмета. Так же, как и в случае с двухмерными объектами (которые мы уже обсуждали в четвертой главе), пространственные объекты мы будем определять в собственных локальных системах координат (0,0,0). Затем, когда мы будем перемещать объект, мы его просто переведем в конечную позицию.



Для наших структур это будет точка (xo,yo,zo). Решением этой задачи будет простой перенос каждой из точек объекта, так же, как мы это делали для двухмерных объектов. Мы можем проверить этот метод и для объемных фигур. Например, представим себе куб, с вершиной в точке (2,2,2) (см. рис. 6.4). Если мы посмотрим на куб, то увидим, что он состоит из восьми вершин и шести поверхностей. Используя наши структуры данных, мы можем описать куб как объект с шестью гранями. Проблема, возникающая в данном случае, состоит в том, что это не самый лучший способ описания объекта. Ведь любая поверхность ограничена четырьмя точками и каждая из этих точек является общей еще для двух поверхностей. Это значит, что описание избыточно.

Возможно, более удачной окажется структура данных, содержащая список вершин. В этом случае избыточности не возникает. Однако при этом структура станет более общей и сложной, поскольку:
  • Мы должны будем иметь указатели либо индексы, или то и другое вместе для ссылки на вершины, необходимые для построения геометрической фигуры. Это увеличивает время распознавания данных объектов;
  • Наши структуры могут использовать заранее определенные массивы для хранения вершин и многоугольников. Это неэффективно использует па­мять. Массивы должны быть одного размера, так как, независимо от того, используем ли мы один элемент массива или весь массив, нам необходимо отводить место под максимальное число элементов.

Эти факты надо принимать во внимание, когда создаете структуры для трехмерных объектов. Таким образом, для наших целей структуры данных из Листингов 6.2 и 6.3 являются наиболее простыми для работы. Если же вы хотите создать набор реальных трехмерных структур, то должны использовать другую тактику.

В общем случае представление двух- и трехмерных объектов сильно зависит от игры, которую вы пишете, от размера используемой памяти и т. д. (Наша цель сейчас - понять механизмы трехмерной графики и рендеринга, а не поиск наиболее эффективных способов представления данных в компьютере. Это зависит от используемых алгоритмов и структур данных.)

Просуммируем все вышесказанное:
  • Трехмерные объекты состоят из вершин;
  • Эти вершины соединяются поверхностями или многоугольниками, которые задают границы объекта;
  • Объекты описываются относительно начала координат;
  • Существует много способов представления трехмерных объектов и вы должны выбрать тот, который устраивает вас по скорости и объему памяти.


Перемещения, масштабирование и повороты в трехмерном пространстве

Все объекты в видеоиграх представлены точками, линиями и геометрическими фигурами. Потому мы должны всегда производить их разбиение на исходные составляющие — точки. Только проделав это, мы можем их преобразовывать. Поскольку точки — это вершины объектов, то они могут рассматриваться вообще отдельно от объекта, но при этом представлять его в целом. Например, если мы хотим повернуть или переместить куб, то, прежде чем выполнять данное преобразование, нам нужно разбить объект на многоугольники, а затем на точки.

Понимая это, мы должны сосредоточить свое внимание на способах преобразования точек.

Перемещение трехмерного объекта

Для перемещения точки (x.y.z) на расстояние (dx,dy,dz) необходимо выполнить следующие операции:

x=x+dx;

y=y+dy;

z=z+dz;

Если мы хотим использовать эту матрицу, то должны представить точку в виде четырех компонентов (x,y,z, 1). Матричное умножение будет выглядеть так:



где dx, dy и dz - это перемещения по осям координат, а х', у' и z' - координаты точки после перемещения.

Масштабирование трехмерного объекта

Следующая операция трансформации, которую должны уметь выполнять, это масштабирование. Изменение размеров трехмерного объекта похоже на двухмерное масштабирование. Здесь показано масштабирование точки (x,y,z) с коэффициентом S:

х=х * S;

у=у * S;

z=z * S;

Все очень просто. Только кажется, что трехмерная графика сложна для понимания. Для описания преобразований с помощью матриц, мы опять должны представить точку в виде (x,y,z,1):



Если вы решите масштабировать каждый из компонентов по-разному, то вам потребуются разные коэффициенты для каждого измерения:



Это приведет к неоднородному масштабированию.

Вращение трехмерного объекта

Последняя трансформация, о которой мы поговорим - это вращение. Вращение трехмерного объекта аналогично двухмерной ротации; необходимо только добавить третье измерение.

Вращение, параллельное оси Х

Следующая матрица преобразований вращает точку (x,y,z) параллельно оси X:



Вращение, параллельное оси Y

Матрица преобразования, вращающая точку параллельно оси Y:



Вращение, параллельное оси Z

Матрица преобразования, вращающая точку параллельно оси Z;



Последнее слово о трехмерных трансформациях

Подведем итог разговору о трансформациях трехмерных объектов. Реализовать эти трансформации не сложно. Это только часть возможных преобразований. Существует много вариаций и несколько совершенно новых типов — деформация и т. д. В общем, мы уже достаточно знаем, чтобы перейти к проекциям.

Проекции

Сейчас мы знаем, как изобразить трехмерный объект и как произвести операции перемещения, масштабирования и поворота этого объекта. Возникает вопрос: «А как мы можем рисовать трехмерные объекты на плоском экране?» ответ прост: мы «проецируем» их на поверхность экрана.

Проекция трехмерных объектов на плоскость довольно проста и дает хорошие результаты. К сожалению, образы при этом не всегда выглядят реалистично. Ваши глаза имеют одну точку зрения, а, следовательно, образ будет похож на трехмерный, но не станет «по-настоящему» объемным.

Замечание

Существуют специальные шлемы с дисплеями, дающие стереоскопический эффект. Эта шлемы используются в системах виртуальной реальности. Проблема, возникающая с их использованием, заключается в том, что параллакс или фокальная точка различна для каждого человека. Если шлем не отрегулирован и точка не подобрана, то зритель вскоре получит сильнейшую головную боль.


Тем не менее, мы будем использовать монитор для проецирования трехмерного образа на экран. Я хотел бы обсудить типы проекции, которые могут применяться с этой целью: параллельная, или ортогональная проекция и перспективная проекция. Рисунок 6.5 показывает диаграммы каждого типа проекций.

Параллельная проекция проста в реализации, но образы не выглядят объемными. Они, скорее, похожи на обычные плоские картинки. Для реализа ции такой проекции достаточно убрать Z-компонент каждой точки трехмерного объекта и затем нарисовать объект, как двухмерный.

С другой стороны, перспективная проекция дает большее приближение и выглядит почти «трехмерно». Она имеет качество «длинной дороги». На рисунке 6.6 изображена такая «дорога». Перспективная проекция принимает во внимание 2-компонент и соответст­венно изменяет компоненты Х и Y.

Элементы, которые подвергаются «перспективному» преобразованию, должны:
  • Просто делиться или умножаться на Z-компонент;
  • Обладать дистанцией просмотра.

Вскоре мы затронем детали проецирования, но сначала давайте поговорим об экране и его взаимоотношениях с координатами трехмерного пространства.







Копирование на экран

Создание настоящей трехмерной графической системы — довольно сложное дело. Этому посвящены целые книги (еще толще, чем та, которую вы держите в руках). В первом приближении нам нужны:
  • Кое-какие трехмерные объекты;
  • Объем просмотра;
  • Проекция на план (плоскость) просмотра.

Мы можем быть спокойны относительно трехмерных объектов — мы определили многоугольники как множество вершин и построили на их основе объекты. А вот что касается объема просмотра и плана просмотра, то тут требуются некоторые разъяснения.



Когда мы смотрим на трехмерный мир, то сами должны в нем где-то находиться и иметь направление просмотра. Рисунок 6.7 иллюстрирует эту мысль.

Учитывая, что в реальном мире мы смотрим на объекты с определенной точки и в определенном направлении, мы можем их построить на экране, который и будет являться планом просмотра. Такой подход довольно сложен, поскольку требует выполнения множества преобразований для решения данной задачи в общем виде. Но, поскольку мы программисты, нас не очень должно волновать общее решение задачи. Нам надо создавать игры, выглядящие как трехмерные.
  • Во-первых, мы делаем допущение, что центр экрана совпадает с центром системы координат. Рисунок 6.8 показывает, как она выглядит на экране;
  • Во-вторых, мы предполагаем, что игрок будет смотреть на экран вниз по оси Z (отрицательное направление).

Мы можем спроецировать одиночный пиксель в точку (0,0,0) на экран, используя параллельную проекцию, и увидеть точку в центре экрана. Рисунок 6.9 показывает разницу между параллельной и перспективной проекциями. Вы можете заметить, что центр проекции - это дистанция между игроком и планом просмотра (поверхностью экрана).

Математика для проецирования трехмерных объектов на экран сказочно проста и мы ее реализуем за пару минут. Но пока поговорим о масштабировании.



Масштабирование экрана

Поскольку в режиме 13h экран имеет размеры 320х200 точек, то возникает вопрос: «Как мы сможем увидеть вселенную размером 1000х1000 точек?» Есть много способов решения этой проблемы. Один из путей - это использование перспективной проекции. Когда мы удаляемся от объекта, он становится меньше. В некоторой точке вселенная размером 1000х1000 уменьшается настолько, что «влезает» в экран.

Если вы используете параллельную проекцию, вам не удастся добиться такого результата. Чтобы сделать это, вы должны масштабировать экран и использовать его с другими размерами. Например, для получения экрана размером 1000х1000 мы должны сделать следующее.

Чтобы нарисовать точку (х,у) на экране, где каждый из компонентов имеет диапазон измерения от 0 до 1000, мы производим преобразования

x_screen = 320 * х / 1000;

y_screen = 200 * у /1000;

где х_screen и y_screen будут истинными координатами пикселя на экране размером 320х200.

Конечно, изменив таким образом масштаб экрана, вы не получите разрешения 1000х1000 точек. Разрешающая способность останется прежней - 320х200. Более того, некоторые точки будут изображены в одних и тех же местах. Например, точки с координатами 999 и 1000 сольются в одну, и вы не сможете их различить. Но это и не столь важно, поскольку трехмерное изображение на основе многоугольников отличается от битовых изображений двухмерного мира.

Математические основы параллельных проекций

Математика, необходимая для произведения параллельной проекции, элемен­тарна - вы просто убираете z-координату и рисуете каждую точку объекта по координатам (х,у). Вот и вся трансформация. Для примера возьмем точку (x,y,z).

x_parallel = х

y_parallel= у

plot х,у

Математические основы верспективной проекции

Произведение перспективной трансформации не многим отличается от параллельной проекции. Мы используем z-компонент для масштабирования координат х и у с целью «отдаления» точки от плана просмотра (поверхности экрана), и для придания объекту более реалистичного вида.

Рассмотрим эту операцию на примере: дана точка (x,y,z), удаленная от плана просмотра на расстояние D:

x_perspective = D * х / z

y_perspective = D * у / z

Вот и все. Достаточно умножить каждый компонент на расстояние и разделить на значение Z-координаты. Образ получается похожим на трехмерный.

Объем просмотра

Мы говорили об объектах и проекциях в трехмерном пространстве, но не упоминали об объеме просмотра. Это понятие подобно области отсечения для двухмерного мира (мы это обсуждали в четвертой главе, «Механизмы трехмерной графики»). Так же, как мы отсекаем объекты у границ экрана в двухмерных играх, мы должны проделывать это и с трехмерными объектами. Видимая часть пространства имеет шесть поверхностей и чем-то напоминает трехмерную трапецию или усеченную пирамиду (см. рис. 6.10). Эта область иногда называется еще телесным углом просмотра.



В общем случае, нам нужно отсечь каждый объект или его часть, выходящую за границы телесного угла просмотра. Поскольку отсекаемые фрагменты при перспективной проекции становятся не видны, то и нет необходимости выполнять их рендеринг.

Наиболее важны ближняя и дальняя плоскости отсечения (рис, 6.10). Когда объект подходит слишком близко к экрану, то он должен исчезать (он как-бы оказывается у вас за спиной), а когда он удаляется на слишком большое расстояние, то мы его тоже можем убрать, поскольку он просто превратится при рендеринге в точку.

Я больше ничего не буду говорить про трехмерное отсечение - оно не потребуется для игр типа DOOM и Wolfenstem, но может пригодиться для создания различных «леталок» и «ездилок». Вы можете использовать ту же тактику, что и для двухмерного отсечения. Теперь нам надо познакомиться еще с одной вещью — моделированием цельных геометрических объектов.

Геометрическое моделирование

Геометрическое моделирование необходимо для придания играм большего реализма. Объекты, построенные компьютером при геометрическом моделировании, выглядит цельным. Посмотрите на рисунок 6.11, чтобы увидеть разницу между цельной геометрической и каркасной моделями.



Как видите, геометрическая модель выглядит более убедительно. Сегодня использование каркасных объектов уже нельзя назвать удовлетворительным решением. Теоретически создание цельных моделей ненамного сложнее, нежели каркасных. Мы можем использовать те же самые структуры данных, преобразования и проекции. Но когда мы пытаемся нарисовать цельный трехмерный объект, возникают определенные проблемы. Как вы понимаете, существуют еще и скрытые поверхности — те, которые не видны игроку (например, тыльная сторона рассматриваемого объекта) и которые не должны появляться на экране. Компьютер не понимает разницы между видимыми и невидимыми поверхностями. Он нарисует их, все. Чтобы избежать этого, мы должны найти способы удаления скрытых поверхностей. Сейчас мы об этом и поговорим.

Удаление невидимых поверхностей

Среди программистов, работающих в области компьютерной графики, техника удаления невидимых поверхностей считается «высшим пилотажем». Известно, что удаление невидимых поверхностей является сложной математической задачей. Существует довольно много алгоритмов для ее решения, но все они очень сложны в реализации и редко работают с приемлемой для видеоигр скоростью.

Удаление невидимых поверхностей можно разделить на две фазы: