Новая версия масштабирования
Прежде чем попытаться реализовать алгоритм отсечения, мы должны разобраться с основами операций масштабирования. Поскольку масштабирование и отсечение идут рука об руку, мы должны сначала написать программу изменения масштаба, а затем адаптировать ее для процесса отсечения.
Механизм масштабирования мы показали в седьмой главе, «Улучшенная битовая графика и спецэффекты» (Листинг 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? Однако пора переходить к алгоритму отсечения.