Программирование игр для Windows. Советы профессионала

       

Новая версия масштабирования


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

Механизм масштабирования мы показали в седьмой главе, «Улучшенная битовая графика и спецэффекты» (Листинг 7.9). Для масштабирования текстуры стен в этом алгоритме был использован не совсем обычный подход. Каждая стена рассматривалась не как обычная двухмерная матрица, имеющая высоту и ширину, а разбивалась на отдельные одномерные столбцы. В результате программе нужно было масштабировать только столбцы шириной в один пиксель. (Кстати, простой текстурированный пиксель называется текстелем (textel).)

Для реализации двухмерного масштабирования мы слегка модифицируем код, представленный в Листинге 7.9. В новом варианте будут использованы только целые числа, а индексы масштабирования будут предварительно подсчитаны и занесены в таблицу соответствий. Текст новой программы масштабирования приведен в Листинге 8.1.

Листинг 8.1. Новая функция масштабирования спрайтов (без отсечения).

void Scale_Sprite(sprite_ptr sprite,int scale)

//Эта функция масштабирует спрайт (без отсечения). Масштабирование

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

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

// столбец. Затем другая таблица используется для учета

// масштабирования этих столбцов по оси Х

char far *work_sprite; // текстура

спрайта

int *row_у;             // указатель на масштабированные

         // по оси Y

данные (заметьте, что это

// ближний указатель)



int far

*row_x;         // указатель на масштабированные

   // по оси Х данные (заметьте, что это

// дальний указатель)

unsigned char pixel;    // текущий текстель

int    x,                   // рабочие переменные

y,

column, work_offset, video_offset, video_start;

// если объект слишком мал, то и рисовать его не стоит




if (scale<1) return;

// рассчитываем необходимые для масштабирования данные

row_у = scale_table_y [scale];

row_x = scale_table_x[scale];

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

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

// рассчитываем

начальное смещение

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

// изображение рисуется слева направо и сверху вниз

for (х=0; x<scale; x++)

{

// пересчитываем адрес следующего столбца

video_offset = video_start + x;

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

// исходя из индекса масштабирования по оси Х

column = row_x[x];

// наконец рисуем столбец обычным образом

for (y=0; y_scale; y++)

{

// проверка на "прозрачность"

pixel = work_sprite[work_offset+column];

if (pixel)

double_buffer[video_offset] = pixel;

// индекс следующей строки экрана и смещение в области

// хранения

текстуры

video_offset += SCREEN_WIDTH;

work_offset = row_y[y] ;

} // конец цикла по У

} // конец цикла по Х

} // конец Scale_Sprite

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

Отметим, что таблицы соответствия находятся в разных сегментах памяти: ближнем (NEAR) и дальнем (FAR). Это сделано для скорости. Таблица соответствия во внутреннем цикле масштабирования (по оси У) должна быть в ближнем сегменте данных для ускорения доступа. Таблица соответствия во внешнем цикле (масштабирование по оси X) может располагаться в дальнем сегменте, так как доступ к ней обычно осуществляется только несколько десятков раз.

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


Основное правило при работе с таблицами соответствий на языке Си состоит в следующем: пытайтесь расположить наиболее часто используемые таблицы в ближнем сегменте, а менее употребимые — в дальнем. Это правило не касается того случая, когда есть возможность уместить и таблицу, и глобальные переменные в 64К ближнего сегмента данных.

Теперь посмотрим на нашу новую процедуру масштабирования в действии. Я написал программу, которая загружает ряд заранее отсканированных изображений размером 80х48 пикселей. Эти изображения были сделаны с помощью макета космического корабля, который фотографировался под разными углами в моей импровизированной студии (о ней я уже рассказывал в начале главы). Эта программа (VYREN.C) показывает вращающийся на некотором расстоянии от наблюдателя (то есть от вас) космический корабль и позволяет передвигать его по оси Z с помощью клавиш > (правая угловая скобка или «меньше») и < (левая угловая скобка или «больше»), (На самом деле, в нижнем регистре это будут клавиши с символами запятой и точки соответственно.) Вы увидите, что новый метод с использованием таблиц существенно быстрее прежнего.

Текст этой программы приведен в Листинге 8.2. Потратьте некоторое время и постарайтесь понять, как работает эта программа. Как обычно, программа должна быть скомпонована с нашей растущей графической библиотекой, GRAPHICS.С, и скомпилирована с использованием модели памяти MEDIUM. Для завершения работы программы, следует нажать клавишу Q.

Листинг 8.2. Демонстрационная программа новой функции масштабирования (VYREN.C).

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

#include <io.h>

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

#include <dos.h>

#include <bios.h>

#include <fcntl.h>

#include <memory.h>

#include <malloc.h>

#include <math.h>

#include <string.h>

#include <graph.h>

#include "graphics.h" // включаем нашу графическую библиотеку



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

void Create_Scale_Data_X(int scale, int far *row);

void Create_Scale_Data_Y(int scale, int * row);

void Build_Scale_Table(void);

void Scale_Sprite(sprite_ptr sprite,int scale);

void Clear_Double_Buffer(void) ;

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

#define MAX_SCALE      200      // число звезд на звездном небе

#define SPRITE_X_SIZE  80       // максимальные размеры

#define SPRITE_Y_SIZE  48       // растрового изображения

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

sprite object;                       // обобщенный спрайт, который

// содержит кадры с космическим

// кораблем

pcx_picture text_cells;// PCX-файл с изображениями

int *scale_table_y[MAX_SCALE+l];     // таблица с предварительно

// рассчитанными коэффициентами

// масштабирования

int far *scale_table_x[MAX_SCALE+l]; // таблица с предварительно

          // рассчитанными коэффициентами

          // масштабирования

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

void Create_Scale_Data_X(int scale, int far *row)

{

// эта функция масштабирует полосу текстуры для всех возможных

// размеров и создает огромную таблицу соответствий

int х;

float x_scale_index=0, x_scale_step;

// рассчитываем шаг масштабирования или число исходных пикселей

// для отображения на результирующее изображение за цикл

x_scale_step = (float)(sprite_width)/(float)scale;

x_scale_index+=x_scale_step;

for (x=0; x<scale; x++)

{

// помещаем данные в массив для последующего использования

row[x] = (int)(x_scale index+,5);

if  (row[x] > (SPRITE_X_SIZE-1)) row[x] = (SPRITE_X_SIZE-1);

// рассчитываем следующий индекс

x_scale index+=x_scale_step;

} // конец

цикла

} // конец Create_Scale_Data_X

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

void Create_Scale_Data Y(int scale, int *row)

{

// эта функция масштабирует полосу текстуры для всех возможных

// размеров и создает огромную таблицу соответствий



int у;

float y_scale_index=0, у

scale_step;

// рассчитываем шаг масштабирования или число исходных пикселей

// для отображения на результирующее изображение за цикл

у_scale_step = (float)(sprite_height)/(float)scale;

y_scale index+=y_scale_step;

for (y=0; y<scale; y++)

{

// помещаем данные в-массив для последующего использования

row[y] = ((int)(y_scale_index+.5)) * SPRITE_X_SIZE;

if (row[y] > (SPRITE_Y_SIZE-1)*SPRITE_X_SIZE) row[y] = (SPRITE_Y_SIZE-1)*SPRITE_X_SIZE;

// рассчитываем следующий индекс

y_scale_index+==y_scale_step;

} // конец

цикла

} // конец Create_Scale_Data_Y //////////////////////////////////////////

void Build_Scale_Table (void)

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

// коэффициентов  масштабирования для всех возможных размеров

// от 1 до 200 пикселей

int scale;

// резервируем память

for (scale=l; scale<=MAX_SCALE; scale++)

{

scale_table_y[scale] = (int *)malloc(scale*sizeof(int)+1);

scale_table_x[scale] = (int far *)_fmalloc(scale*sizeof(int)+l);

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

// создаем таблицу масштабирования для осей X и Y

for (scale=l; scale<=MAX_SCALE; scale++) {

// рассчитываем коэффициент для данного масштаба

Create_Scale_Data_Y(scale, (int *)scale_table_y[scale]);

Create_Scale_Data_X(scale, (int far *)scale_table_x[scale]) ;

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

}// конец Build_Scale_Table ////////////////////////////////////////////////////////////

void Scale_Sprite(sprite_ptr sprite,int scale)

{

// эта функция масштабирует спрайт (без отсечения). Масштабирование производится с //использованием заранее рассчитанной таблицы, которая определяет, как будет изменяться //каждый вертикальный столбец. Затем другая таблица используется для учета //масштабирования этих столбцов по оси Х

char far *work_sprite;  // текстура спрайта

int *row_y;             // указатель на масштабированные

// по оси Y данные (заметьте, что

// это ближний указатель)

int far *row_x;         // указатель на масштабированные

// по оси Х данные (заметьте, что



// это дальний указатель)

unsigned char pixel;    // текущий текстель

lnt x,                  // рабочие переменные

У, column,

work_offset,

video_offset,

video_start;

// если объект слишком мал, то и рисовать его не стоит

if (scale<1) return;

// рассчитываем необходимые для масштабирования данные

row_y = scale_table_y[scale];

row_x = scale table_x[scale];

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

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

// рассчитываем начальное смещение

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

// изображение рисуется слева направо и сверху вниз

for (x=0; x<scale; х++)

{

// пересчитываем адрес следующего столбца

video_offset = video_start + х;

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

// исходя из индекса масштабирования по оси Х

column = row_x[x];

// Наконец рисуем столбец обычным образом

for (y=0; y<scale; y++)

{

// проверка на "прозрачность"

pixel = work_sprite[work_offset+column] ;

if (pixel)

double buffer[video_offset] = pixel;

// индекс следующей строки экрана и смещение

//в области хранения текстуры

video_offset += screen_width;

work_offset  =  row_y[y];

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

} // конец цикла ро Х

} // конец Scale_Sprite

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

void Clear Double_Buffer(void) {

// угадали что это?

_fmemset(double_buffer, 0, SCREEN__WIDTH * SCREEN_HEIGHT + 1);

} // конец Clear_Double_Buffer

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

void main(void)

{

// Загружаем 12 предварительно отсканированных кадров спрайта

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

// координату Z

объекта, нажав клавишу "," или "."

int done=0,                    // флаг завершения

count=0,                   // счетчик времени изменения кадра

scale=64;                  // текущий масштаб спрайта

float scale_distance

= 24000, // произвольная константа

// для согласования



// плоской текстуры и трассированного

// пространства

view_distance

= 256, // дистанция до объекта

х=0,                      // позиция корабля в трехмерном

            // пространстве

у=0,

z=1024;

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

_setvideomode(_MRES256COLOR) ;

sprite_width = 80;

sprite_height =48;

// создание таблицы для подсистемы масштабирования

Build_Scale_Table ();

// инициализация файла PCX, содержащего кадры

PCX_Init((pcx_picture_ptr)&text cells) ;

// загрузка файла PCX, содержащего кадры

PCX_Load("vyrentxt.pcx", (pcx_picture_ptr)&text_cells,1);

// резервируем память под дублирующий буфер

Init_Double_Buffer ();

Sprite_Init((sprite_ptr)&object,0,0,0,0,0,0) ;

// загружаем 12 кадров с космическим кораблем

PCX_Grap_Bitmap ( (pcx_picture_ptr) &text_cells,

(sprite_ptr)&object,0,0,0);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

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

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

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

PCX_Grap_Bitmap ((pcx_picture_ptr)&text_cells,

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

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,(sprite_ptr)&object,4,1,1) ;

PCX_Grap_Bitmap ((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,5,2,1);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,6,0,2);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,7,1,2) ;

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,8,2,2) ;

PCX_Grap_Bitinap( (pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,9,0,3);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,10,1,3);

PCX__Grap_Bitmap( (pcx_picture_ptr) &text_cells,

(sprite_ptr)&object,11,2,3) ;

// начальная позиция корабля

object.curr_frame =0;

object.x          = 0;

object, у          =0;

Clear_Double_Buffer();

// ожидаем нажатия клавиш и рисуем корабль

while(!done)

{

// нажал ли игрок клавишу?



if (kbhit())

{

switch(getch()) {

case '.': // отдаляем корабль

{

z+=16;

} break;  

case ',':// приближаем корабль

{

z-=l6;

// не позволяем кораблю подойти слишком близко

if(Z<256)

z=256;

} break;

case

'q': // выход из программы

{

done=1;

} break;

default:break;

} // конец оператора switch

} // конец оператора if

//рассчитываем размер растрового изображения

scale = (int)( scale_distance/z );

// исходя из размера растрового изображения,

// рассчитываем проекции координат Х и Y

object.x= (int)((float)x*view_distance / (float)z) + 160 - (scale>>1);

object.y = 100  - (((int)((float y*view_distanc=e / (float)z) + (scale>>1)));

// увеличиваем счетчик кадров

if

(++count==2)

{

count=0;

if (++object.curr_frame==12) object.curr_frame=0 ;

} // конец оператора if

// очищаем дублирующий буфер

Clear_Double_Buffer();

// масштабируем

спрайт

Scale_Sprite((sprite_ptr)&object,scale);

Show_Double_Buffer(double_buffer);

// выводим информацию на экран

_settextposition(24,0) ;

printf("z Coordinate is %f",z);

} // конец оператора while

// Удаляем файл PCX

PCX_Delete((pcx_picture_ptr) &text_cells);

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

_setvideomode(_DEFAULTMODE);

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

После выполнения программы из Листинга 8.2, вы, возможно, будете удивлены возможностями оцифровки изображений макетов и приведенным вариантом трехмерной мультипликации. Кто знает, может быть вы создадите что-нибудь именно по типу игры Wing Commander, а вовсе не очередную вариацию DOOM? Однако пора переходить к алгоритму отсечения.


Содержание раздела